【问题标题】:Unexpected outcome with merging react state合并反应状态的意外结果
【发布时间】:2019-05-05 14:17:56
【问题描述】:

一旦找到并显示天气,点击摄氏度应该会运行unitHandler,然后将转换温度值,然后更新状态。但是,当更新this.state.currentWeather.temp(一个已经存在的属性,所以我认为它会进行“浅”合并,并且只是“更新”状态)时,它会清除当前存在的其余状态属性。

我想知道为什么它没有像 React Docs 显示 here 的示例那样进行“浅层”合并,而是清除了我的其余状态?是不是因为 React 有时会批处理多个 setState() 调用以提高性能,如下面的文档所示?

状态更新可能是异步的 React 可能批处理多个 setState() 调用单个更新以提高性能。

因为 this.props 和 this.state 可能是异步更新的,所以你 不应依赖它们的值来计算下一个状态。

我想我只是感到困惑,因为在文档的正下方,它说在更新/合并时它将保持其他状态的其余部分完好无损:

状态更新被合并当你调用 setState() 时,React 会合并 您提供的对象进入当前状态。合并很浅,所以 this.setState({cmets}) 保持 this.state.posts 完整,但是 完全取代 this.state.cmets。

做一些研究,我认为可以防止这种情况发生的一种方法是将prevState 函数传递给this.setState,但是,我无法使用扩展运算符正确编写函数。

const root = document.querySelector('.root');

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      unit: '',
      currentWeather: {
        main: '',
        desc: '',
        temp: '',
      }
    }
    
    this.getWeather = this.getWeather.bind(this);
    this.unitHandler = this.unitHandler.bind(this);
  }
  
  getWeather(e) {
    e.preventDefault();
    const city = e.target.elements.city.value;
    const country = e.target.elements.country.value;
    const appID = 'bf6cdb2b4f3c1293c29610bd1d54512b';
    
      const currentWeatherURL = `https://api.openweathermap.org/data/2.5/weather?q=${city},${country}&units=imperial&APPID=${appID}`;
    const forecastURL = `https://api.openweathermap.org/data/2.5/forecast?q=${city},${country}&units=imperial&APPID=${appID}`;
    
    //fetch CURRENT weather data ONLY
    fetch(currentWeatherURL)
      .then((response) => response.json())
      .then((data) => {
        this.setState({
          unit: '°F',
          currentWeather: {
           main: data.weather[0].main,
           desc: data.weather[0].description,
           temp: data.main.temp,
          }
        });
    })
    .catch(() => {console.log('something went wrong, but we caught the error')});
  }
  
  unitHandler(e) {
    function convertToCelsius(fahrenheit) {
      return ((fahrenheit-32)*5/9)
    }
    
    function convertToFahrenheit(celsius) {
      return ((celsius*9/5) + 32)
    }
    
    //if fahrenheit is checked
    if(e.target.value === 'fahrenheit') {
      const fahrenheitTemp = convertToFahrenheit(this.state.currentWeather.temp);
      this.setState({unit: '°F',currentWeather: {temp: fahrenheitTemp}});
    } 
    //otherwise, celsius is checked
    else {
      const celsiusTemp = convertToCelsius(this.state.currentWeather.temp);
      this.setState({unit: '°C', currentWeather: {temp: celsiusTemp}});
    }
  }
  
  render() {
    console.log('handler state');
      console.log(this.state);
    return (
      <div className='weather-app'>
        <LocationInput getWeather={this.getWeather} unitHandler={this.unitHandler}/>
        <CurrentWeather weatherStats={this.state.currentWeather} unit={this.state.unit} />
      </div>
    )
  }
}

// Component where you enter your City and State 
function LocationInput(props) {
  return (
    <div className='location-container'>
      <form className='location-form' onSubmit={props.getWeather}>
         <input type='text' name='city' placeholder='City'/>
         <input type='text' name='country' placeholder='Country'/>
        <button>Search</button>
        <UnitConverter unitHandler={props.unitHandler} />
      </form>
    </div>
  )
}

// Component to convert all units (fahrenheit <---> Celsius)
function UnitConverter(props) {
  return (
    <div className='unit-converter' onChange={props.unitHandler}>
      <label for='fahrenheit'>
        <input type='radio' name='unit' value='fahrenheit' defaultChecked/>
        Fahrenheit
      </label>
      <label for='celsius'>
        <input type='radio' name='unit' value='celsius'/>
        Celsius
      </label>
    </div>
  )
}

// Base weather component (intention of making specialized components for weekly forecast)
function Weather (props) {
  console.log('component state');
  console.log(props);
   const icons = {
        thunderstorm: <i class="fas fa-bolt"></i>,
        drizzle: <i class="fas fa-cloud-rain"></i>,
        rain: <i class="fas fa-cloud-showers-heavy"></i>,
        snow: <i class="far fa-snowflake"></i>,
        clear: <i class="fas fa-sun"></i>,
        atmosphere: 'No Icon Available',
        clouds: <i class="fas fa-cloud"></i>,
      };
  
  let currentIcon = icons[props.weatherStats.main.toLowerCase()];

  return (
    <div className={'weather-' + props.type}>
      <h1>{props.location}</h1>
      <h2>{props.day}</h2>
      <figure className='weather-icon'>
        <div className='weather-icon'> 
          {currentIcon}
        </div>
        <figcaption>
          <h3 className='weather-main'>{props.weatherStats.main}</h3>
          <div className='weather-desc'>{props.weatherStats.desc}</div>
          {props.weatherStats.temp && <div className='weather-temp'>{Math.round(props.weatherStats.temp)}{props.unit}</div>}
        </figcaption>
      </figure>      
    </div>
  ) 
}

// Using the specialization concept of React to create a more specific Weather component from base
function CurrentWeather(props) {
  const dateObj = new Date();
  const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
  const currentDay = days[dateObj.getDay()];
  
  return (
    <Weather 
      type={'current'} 
      weatherStats={props.weatherStats} 
      day={currentDay}
      unit={props.unit}
      />
  )
}

ReactDOM.render(<App />, root);
.weather-app {
  text-align: center;
}

.weather-current {
  display: inline-block;
}

.wf-container {
  display: flex;
  justify-content: center;
  align-items: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div class="root"></div>

【问题讨论】:

  • 请在此处添加所有相关代码,无需去外部站点查看您的尝试
  • @Icepickle 抱歉,已更新。
  • React 浅合并状态,这意味着它只查看状态对象的顶级属性,但 temp 嵌套在 currentWeather 中,react 将完全替换。

标签: javascript reactjs


【解决方案1】:

那是因为你完全替换了当前的天气对象。您必须保留其他当前天气属性才能使其正常工作:

this.setState((state) => ({
    unit: '°C',
    currentWeather: {
        ...state.currentWeather,
        temp: celsiusTemp
    }
}));

当然,你必须对其他转换方法做同样的事情。

Here 是工作示例。

【讨论】:

  • 正确,但需要注意的是,根据当前状态或道具更新状态时,您应该使用setState的回调版本,否则在批处理时可能会覆盖之前的更改。跨度>
  • 这取决于脚本的逻辑。如果您更新 currentWeather 对象中的多个属性,那么您当然需要确保所有更改都设置在 state 中。
  • 也许吧,但我认为使用回调是一个好习惯。文档说:“如果您需要根据当前状态计算值,请传递更新程序函数而不是对象”。当前更新仅在事件处理程序内部进行批处理,但这是一个您不应该依赖的实现细节,并且可能会在未来的 react 版本中发生变化。如果您在从当前状态更新时不使用回调,您的代码可能会中断。
  • @trixn 完成.. 你是对的。我认为这是新版本的 react 中的建议?我以前从没听说过..
  • 自从我开始使用 react 大约两年后,该建议就一直存在。但它非常隐蔽,很容易被忽视。供参考:reactjs.org/docs/…
【解决方案2】:

您的 setState() 应该如下所示:

this.setState(prevState => ({ 
  ...prevState, 
  currentWeather: { ...prevState.currentWeather, temp: celsiusTemp } 
}));

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-06-06
    • 1970-01-01
    • 1970-01-01
    • 2018-01-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-27
    相关资源
    最近更新 更多