10.React中文之提升状态

来源:互联网 发布:centos 6 vsftp 配置 编辑:程序博客网 时间:2024/06/16 02:08

经常,几个组件需要反映相同变化数据。我们建议将共享状态提升至最近的共同父组件。让我们看看如何起作用。

在这节中,我们将创建一个温度计算器,计算水在给定温度是否沸腾。

我们将以BoilingVerdict组件开始。它接受celsius温度作为属性,打印温度是否足够沸腾水:

function BoilingVerdict(props){

   if(props.celsius>=100){

        return <p>The water would boil.</p>;

   }

   return <p>The water would not boil</p>;

}

接下来,我们将创建一个叫Calculator的组件。这个组件渲染可输入温度的<input>和保存它的值在this.state.temperature.

另外,它渲染BoilingVerdict显示当前输入的值。

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(

           <fieldset>

                   <legend> Enter temperature in Celsius:</legend>

                   <input value={temperature} onChange={this.handleChange}/>

                   <BoilingVerdict  celsius={parseFloat(temperature)}/>

           </fieldset>

       );

   }

}

在CodePen上试试吧。

我们的新需求是 除了Celsius输入,我们提供一个Fahrenheit输入,他们保持同步。

我们从Calculator中扩展一个TemperatureInput组件。我们将增加一个新的scale属性给这个组件,scale的属性是“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(

              <filedset>

                      <legend>Enter temperature in {scaleNames[scale]}:</legend>

                      <input value={temperature} onChange={this.handleChange}/>

              </filedset>

         ); 

    }

}

我们现在改变Calculator渲染两个分开的温度输入:

class Calculator extends React.Component{

   render(){

        return(

            <div>

                <Temperature scale="c" />

                <Temperature scale="f" />

           </div> 

        );

    }

}

在CodePen上试试吧。

我们现在有两个输入,当你在其中一个里输入温度时,另一个不更新。这与我们的需求矛盾:我们想要它们同步。

我们也无法在Calculator中展示BoilingVerdict。因为当前温度隐藏在TemperatureInput中,Calculator无法获取

写转换函数

首先,我们将写两个函数,将Celsius和Fahrenheit互换。

function toCelsius(fahrenheit){

     return (fahrenheit-32)*5/9;

}

function toFahrenheit(celsius){

     return (celsius*9/5)+32;

}

这俩函数转换数字。我们将写另一个函数,这个函数以一个字符串temperature和一个转换函数为参数,返回一个字符串。我们将基于一个输入来计算另一个输入的值。

给这个函数输入一个无效的temperature返回一个空字符串,这个函数的返回值将保留三位小数。

function tryConvert(temperature,convert){

     const input=parseFloat(temperature);

     if(Number.isNaN(input)){

           return ''; 

     }

     const output=convert(input);

     const rounded=Math.round(output*1000)/1000;

     return rounded.toString();

}

例如,tryConvert('abc',toCelsius)返回一个空字符串,而tryConvert('10.22',toFahrenheit)返回‘50.396’。

提升状态

当前,TemperatureInput组件都独立地放置自己的值在本地状态:

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;

     }

}

然而,我们想要两个输入同步。当我们更新Celsius输入时,Fahrenheit输入应该反映转换的温度,反之亦然。

在React中,共享状态是由技巧的,提升共同状态至需要它的最近的共用父组件。这被称为“提升状态”。我们将从TemperatureInput中移除本地状态,取而代之将它移动至Calculator。

如果Calculator有共享状态,它将变成两个输入的当前温度的真实材料。它将指导他们都有一致的值。因为TemperatureInput组件的属性都来自相同父组件Calculator,这两个输入总是同步。

让我们看看这是如何一步一步工作的。

首先,我们在TemperatureInput组件中用this.props.temperature代替this.state.temperature。现在,我们先假装this.props.temperature已经存在,因为我们之后会在Calculator中传递它。

render(){

     //Before:const temperature=this.state.temperature;

    const temperature=this.props.temperature;

}

我们知道props是只读的。当temperature是本地状态,TemperatureInput只能调用this.setState()更新它。然而,现在temperature是从父类传递来的属性,TemperatureInput无法控制它。

在React中,经常是通过使一个组件变成可控的来解决。像接受value和onChange属性的DOM元素<input>一样,自定义的TemperatureInput接受父组件Calculator的temperature和onTemperatureChange属性。

现在,TemperatureInput想要更新温度,它调用this.props.onTemperatureChange:

handleChange(e){

      //Before:this.setState({temperature:e.target.value});

      this.props.onTemperatureChange(e.target.value);

}

注意:

在自定义组件中,temperature和onTemperatureChange属性名都没有特殊意义。我们可以称他们为其他任何名字,像通用的value和onChange。

onTemperatureChange属性和temperature属性由父组件Calculator一起提供。Calculator组件通过修改它自己的状态来处理变化,这样就可以用新的值重新渲染输出。我们不就将看到新的Calculator的实现。

在Calculator中加入变化前,我们回顾下我们对TemperatureInput组件的改变。我们已经从TemperatureInput组件中去掉了本地状态,代替读取this.state.temperature,我们现在读取this.props.temperature。当我们想要做个改变时,代替调用this.setState(),我们现在调用由Calculato提供的this.props.onTemperatureChange()。

class TemperatureInput extends React.Component{

    constructor(props){

        super(props);

        this.handleChange=this.handleChange.binde(this);

    }

    handleChange(e){

        this.props.onTemperatureChange(e.target.value);

    }c

    render(){

        const temperature=this.props.temperature;

        const scale=this.props.scale;

        return(

             <fieldset>

                     <legend>Enter temperature in {scaleNames[scale]}:</legend>

                     <input  value={temperature} onChange={this.handleChange}/>

            </fieldset> 

       );

    }

}

现在让我们转向Calculator组件。

我们将存储当前输入的temperature和scale在本地状态。这是我们从输入元素中提取出来的状态,这个状态也起输入元素的真实资源的作用。这是为了渲染两个输入元素我们需要知道的所有数据的极小代表。

例如,如果我们在Celsius输入37,Ccalculator组件的状态将是:

{

   temperature:'37',

    scale:'c'c

}

如果我们以后在Fahrenheit中输入的是212,Calculator组件的状态将是:

{

    temperature:'212',

    scale:'f'

}

我们可以存储两个输入元素中的值,但那是不必要的。存储最新改变的输入值和它代表的scale就够了。我们可以根据当前的temperature和scale推算出其他输入的值。

输入保持同步,因为它们的值是从相同的状态中计算得到的:

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'};

    }

    handelCelsiusChange(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(

             <div>

                  <TemperatureInput scale="c" temperature={celsius} onTemperatureChange={this.handleCelsiusChange} />

                  <TemperatureInput scale="f" temperature={fahrenheit} onTemperatureChange={this.handleFahrenheitChange} />

                  <BoilingVerdict celsius={parseFloat(celsius)} />

             </div> 

         );

    }

}

在CodePen上试试吧。

现在,无论你编辑哪个输入框,this.state.temperature 和 this.state.scale在Calculator都会被更新。输入框中的一个得到值是这样,另一个输入框的值总会基于它重新计算出来,因此任何用户的输入都是隐蔽的。

我们回顾一下你编辑一个输入框时发生了什么:
~React调用在DOM<input>上指定给onChange的函数。在我们的例子中,这是在TemperatureInput组件中的handleChange方法。
~在TemperatureInput组件的handleChange方法调用带有可能的新值的this.props.onTemperatureChange(),它的属性,包括onTemperatureChange,由父组件Calculator提供。
~假设Calculator渲染,它已经指定Celsius Temperature 的onTemperatureChange是Calculator的handleCelsiusChange方法,Fahrenheit TemperatureInput 的onTemperatureChange是Calculator的handleFahrenheitChange方法。这两个Calculator方法被调用取决于我们编辑哪个输入框。

~在这些方法中,Calculator组件通过调用this.setState()更新输入值和我们刚编辑的输入框的当前scale要求React重新渲染自己。

~React调用Calculator组件的render方法获悉UI应该像什么样。输入框的值都是基于当前温度和激活的scale被重新计算的。温度转换此时被使用。

~React调用带有由Calculator指定的新属性的独立的TemperatureInput组件的render方法。获悉它们的UI应该是什么样子的。

~React DOM更新DOM去匹配想要得到的值。我们编辑的输入框获取当前值,另一个输入框转换后更新温度。

每次更新都进行相同的步骤以保证输入同步。

经验教训

在React应用中任何变化的数据都应该有一个单独数据源。一般情况下,状态首先加在需要它渲染的组件中。然后,如果其他组件也需要,你可以提升它为最接近的父状态。代替视图在两个不同的组件中同步状态,你应该依赖于top-down data flow.

提升状态相比于双向绑定处理要写更多引用,但是有一点好处,很容易找bugs和绝缘bugs。因为任何状态都存在在一些组件中且只能这些组件更新它,bugs的表面范围被极大的缩减。另外,你可以实现拒绝或者转变用户输入的自定义逻辑。

如果某些东西是由props或者state衍生的,它可能不应该在状态中。例如,代替存储celsiusValue和fahrenheitValue,我们只存储最后编辑的temperature和scale。在render()方法中的另一个值总是由它俩计算出来。在用户输入中,这让我们不丢失任何精度地清空或四合五入应用到另一个字段。

当你在UI中看见一些错误,你可以使用React Developer Tool去检查属性和移动tree直到你发现负责更新状态的组件。这让你到他们的源码中追踪bugs。