状态提升
使用React经常会遇到几个组件需要共用状态数据的情况。这种情况下,我们最好把这部分状态数据提升到他们最近的父组件中进行管理。
这里我们会创建一个温度计算器来计算水是否烧开。
①首先创建一个BoilingVerdict
的组件,它会接收celsius这个温度变量作为它的props属性,最后根据温度判断返回的内容。
function BoilingVerdict(){
if(props.celsius>=100){
return 水开了
}else{
return 水没开
}
}
②接下来我们创建一个 Calculator
组件,他会渲染一个input
来接收用户输入,然后将用户的值保存在this.state.temperature
之后会根据输入的值 渲染会 BoilingVerdictd
的内容
class Calculator extends React.Component{
constructor(props){
super(props);
this.handleChange=this.handleChange.bind(this);
this.state={temperature:''};
}
handleChange(e){
this.setState({temperature:e.target.value})
}
render(){
const temperature = this.state.temperature;
return(
)
}
}
③添加第二个输入框
现在我们又一个新的需求,添加一个输入华氏度的输入框,并且让他与摄氏度保持同步。
我们可以通过Temperature
组件中抽离一个TemperatureInput
组件,给他添加c
或者f
来表示温度单位的属性。
//定义温度单位和标识
const scaleNames={
c:'Celsius',
f:'Fahrenheit'
}
class TemperatureInput extends React.Component{
constructor(props){
super(props);
this.handleChange=this.handleChange.bind(this);
this.state={temperature:''}
}
handleChange(e){
this.setState({temperature:e.target.value});
}
render(){
const temperature = this.state.temperature;
const scale = this.props.scale;
return (
)
}
}
class Calculator extends React.Component{
render(){
return(
)
}
}
我们现在有了两个输入框,但是当你在其中一个输入时,另一个并不会更新。这显然是不符合我们的需求的,还缺少温度转换功能。
另外,我们此时也不能从 Calculator 组件中展示 BoilingVerdict 的渲染结果。因为现在表示温度的状态数据只存在于 TemperatureInput 组件当中。
④添加转换函数
首先我们先写2个可以互换转换摄氏度与华氏度的函数。
function toCelsius(fahrenheit){
return (fahrenheit-32)*5/9;
}
function toFahrenheit(celsius){
return (celsius*9/5)+32;
}
这两个函数目前只是单纯的数字转换,我们还需要另外一个函数,接收这两个参数。
第一个接收temperature
的变量,就是this.state.temperature
的值。
第二个接收上面的转换函数。
最后会返回一个字符串,我们用它来计算另一个输入框的值。
我们最后取到小数点后三位,当temperatue
的值不合法的时候,会返回空字符串。
//第一个参数是输入的温度值 this.state.temperature
//第二个参数是温度转换的方法
function tryConvert(temperature,convert){
const input = parseFloat(temperature);
//如果是非数字,返回空
if(Number.isNaN(input)){
return ''
}
// convert()方法代表以上2个转换函数
const output = convert(input);
const rounded = Math.round(output*1000)/1000;
return rounded.toString();
}
举两个例子,tryConvert('abc', toCelsius)
会返回空字符串,而tryConvert('10.22', toFahrenheit)
会返回 '50.396'。
⑤状态提升
到目前为止 2个TemperatureInput
组件都是在自己的this.state.temperature
状态中独立保存数据的。
但是我们需要两个输入框保持同步,当我们输入摄氏度的同时更新华氏度,反之亦然。
在React中,状态的分享是通过将state中的数据提升至最近的父组件来完成的。就是所谓的状态提升。
我们会将TempetatureInput
组件自身保存的 state 转移到 Calculator
中。
如果当Calculator
组件拥有了提升上来的共享状态数据。他就会成为2个温度输入组件的数据源。他会传入给下面的温度组件同样的数据,由于2个TemperatureInput
温度组件的props属性来源都是父组件Calaulator
,所以他们的数据也会同步。
①首先我们将TemperatureInput
中的this.state.temperature
替换为this.props.temperature
,现在我们假定this.props.temperature
的数据已经存在了,不过之后仍然是需要通过Calculator
组件中传递进去的。
render(){
//之前的代码
//const temperature = this.state.temperature;
const temperature = this.props.temperature;
}
首先我们明白,props是只读的,然而之前的temperature
变量是被保存在自身的状态中,Temperature
组件中只要调用this.state.setState
就能改变他(这一点类似小程序)。
但是现在修改后,使用props,他是作为从父组件传递下来,并且只读,TemperatureInput
没有对该数据的控制权,不能改变他。
在React中,这个问题通常是通过组件“受控”来解决的,就像input
能够接受value
和onChange
这2个props的属性值,所以自定义组件TemperatureInput
也能够接收父组件Calaulator
的Temperature
变量和OnTemperatureChange
方法作为props的属性值。
完成以上这些,当TempertureInput
更新他的温度值时,就会调用this.props.OnTemperatureChange
事件。
OnTemperatureChange
和temperature
这2个props属性都由父组件Calculator
组件提供,父组件是可以通过自身的方法来响应数据的变化。从来使用新的值来重新渲染2分输入框组件。
总结TemperatureInput
组件的变化:
①我们将其自身的 state 从组件中移除,使用 this.props.temperature 替代
②this.state.temperature ,当我们想要响应数据改变时,使用父组件提供的 this.props.onTemperatureChange() 而不是this.setState() 方法:
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
);
}
}
再来看Calaulator组件
我们将会在它的 state 中存储之前输入框组件的 temperature 和 scale 值,这是从输入框组件中“提升”上来的 state,它将会成为两个输入框组件的“数据源”。这是我们所需要的能够重新渲染并且表示两个不同输入组件的最基本的数据。
//举个例子,假如我们在摄氏度输入框中输入37,那么 Calculator 的 state 就是:
{
temperature: '37',
scale: 'c'
}
//如果我们之后在华氏度输入框输入212,那么 Calculator 的状态数据就会是:
{
temperature: '212',
scale: 'f'
}
其实我们可以一起保存两个输入的值,但这么做似乎没有必要。保存最近 改变的值和所需标识的温标单位就足够了。我们可以只需基于当前的 temperature 和 scale 计算出另一个输入框中的值。
现在这两个输入框中的值能保持同步了,因为它们使用的是通过同一个 state 计算出来的值。
完整demo
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
}
function toCelsius(fahrenheit){
return (fahrenheit-32)*5/9;
}
function toFahrenheit(celsius){
return (celsius*9/5)+32;
}
//第一个参数是输入的温度值 this.state.temperature
//第二个参数是温度转换的方法
function tryConvert(temperature,convert){
const input = parseFloat(temperature);
//如果是非数字,返回空
if(Number.isNaN(input)){
return ''
}
// convert()方法代表以上2个转换函数
const output = convert(input);
const rounded = Math.round(output*1000)/1000;
return rounded.toString();
}
class TemperatureInput extends React.Component{
constructor(props){
super(props);
this.handleChange=this.handleChange.bind(this);
// this.state={temperature:''}
}
handleChange(e){
// this.setState({
// temperature:e.target.value
// })
this.props.onTemperatureChange(e.target.value);
}
render(){
// const temperature = this.state.temperature;
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
)
}
}
class Calculator extends React.Component{
constructor(props){
super(props);
this.handleCelsiusChange=this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange=this.handleFahrenheitChange.bind(this);
this.state={temperature:'',scale:'c'};
}
handleCelsiusChange(temperature){
this.setState({
scale:'c',temperature
})
}
handleFahrenheitChange(temperature){
this.setState({
scale:'f',temperature
})
}
render(){
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale==='f'?tryConvert(temperature,toCelsius):temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit):temperature;
return (
)
}
}
ReactDOM.render(
,document.getElementById('root')
)
现在无论编辑哪一个输入框,Calculator
组件中this.state.temperature
都会更新,其中一个输入框得到用户的原始输入值,另一个得到计算后的值。
过程梳理:
①React在DOM原生组件上调用指定的
onChange
函数。在本例中,指的是TemperatureInput
组件上的handleChange
函数。
②TemperatureInput
组件的handleChange
函数会在值发生变化时调用this.props.onTemperatureChange()
函数。这些props
属性,像onTemperatureChange
都是由父组件Calculator
提供的。
③当最开始渲染时,Calculator
组件把内部的handleCelsiusChange
方法指定给摄氏输入组件TemperatureInput
的onTemperatureChange
方法,并且把handleFahrenheitChange
方法指定给华氏输入组件TemperatureInput
的onTemperatureChange
。两个Calculator
内部的方法都会在相应输入框被编辑时被调用。
④在这些方法内部,Calculator
组件会让React使用编辑输入的新值和当前输入框的温标来调用this.setState()
方法来重渲染自身。
⑤React会调用Calculator
组件的render
方法来识别UI界面的样子。基于当前温度和温标,两个输入框的值会被重新计算。温度转换就是在这里被执行的。
接着React会使用Calculator
指定的新props
来分别调用TemperatureInput
组件,React也会识别出子组件的UI界面。
⑥React DOM 会更新DOM来匹配对应的值。我们编辑的输入框获取新值,而另一个输入框则更新经过转换的温度值。
一切更新都是经过同样的步骤,因而输入框能保持同步的。
总结:
在React应用中,对任何可变数据,理应只有一个“数据源”,通常状态数据都是首先添加在需要该数据渲染的组件中的。
此时,如果另一个组件也需要这个状态数据,就可以将这些数据提升到距离他们最近的父组件中进行共用,你应该在应用中保持 自上而下的数据流,而不是尝试在不同组件中同步状态。
状态提升要比双向绑定写更多的模板代码,带来的好处是你也可以更快的定位到bug 的位置,因为哪个组件保有状态数据,也只有他能够操作这些数据,所以bug的位置也被大大的缩小了,而且你还可以根据自己的自定义逻辑来拒绝或者更改用户的输入。
如果某些数据可以由props或者state提供,那他可能不在state中出现。
比如:我们仅仅保存最新的temperature
和scale
的值,而不是同时保存摄氏度和华氏度的值,另一个输入框中,总是可以在render()
函数中根据已经保存的值计算出来。这样我们可以根据用户输入的值精准的计算出剩余的。
当你UI界面开发遇到问题时,你可以使用 React 开发者工具来检查props属性,并且可以点击查看组件树,直到你找到负责目前状态更新的组件。这能让你到追踪到产生 bug 的源头。