经常有些组件需要映射同一个改变的数据。我们建议将共用的state提升至最近的同一个祖先元素。我们来看看这是怎样运作的。

在这一节中,我们会创建一个温度计算器来计算提供的水温是否足够沸腾。

我们先创建一个叫BoilingVerdict的组件。它接受摄氏度温度为prop,并且打印水温是否足够沸腾:

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.value里。

另外,它通过当前输入的温度值渲染BoilingVerdict组件。

class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {value: ''};
} handleChange(e) {
this.setState({value: e.target.value});
} render() {
const value = this.state.value;
return (
<fieldset>
<legend>Enter temperature in Celsius:</legend>
<input
value={value}
onChange={this.handleChange} />
<BoilingVerdict
celsius={parseFloat(value)} />
</fieldset>
);
}
}

在CodePen里试一试

添加第二个输入框

我们新的要求是除了摄氏度的输入之外,我们还提供一个华氏度输入框,并且它们保持同步。

我们可以先从Calculator组件里提取出TemperatureInput组件。并且添加一个新的scale prop给它,可以是“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 = {value: ''};
} handleChange(e) {
this.setState({value: e.target.value});
} render() {
const value = this.state.value;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={value}
onChange={this.handleChange} />
</fieldset>
);
}
}

现在来修改Calculator组件来渲染两种单独的温度输入:

class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}

在CodePen里试一试

现在我们有两种输入了,但是当你在其中一个输入温度后,另外一个却不会更新。这和我们的要求矛盾了:我们想让它们同步。

我们也不能在Calculator里显示BoilingVerdict。Calculator不知道当前的温度因为温度被隐藏在了TemperatureInput里。

写转换函数

首先,我们会写两个函数去将摄氏度和华氏度互相转换:

function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
} function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}

这两个函数转换数字。我们会写另外一个函数,它接收一个字符串值和一个转换函数做为参数并且返回一个字符串。我们用这个函数来根据一个输入来计算另一个输入。

如果最终结果无效它会返回一个空字符串,并且它会将结果四舍五入保存到小数点后第三位:

function tryConvert(value, convert) {
const input = parseFloat(value);
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'。

提升state

现在,两个TemperatureInput组件都独立地将它们的值保存在本地state里:

class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {value: ''};
} handleChange(e) {
this.setState({value: e.target.value});
} render() {
const value = this.state.value;

然而,我们希望这两个输入彼此之间可以同步。当我们更新了摄氏度输入,华氏度输入应该反映出转换后的温度,反之亦然。

在React里,共用的state通过把它传递给组件里最近的共同的祖先来完成。这一步骤叫做“提升state”。我们会从Temperature移除本地state并且把它移到Calculator里。

如果Calcutor拥有了共用的state,它变成了当前两个温度输入的“真正数据源”。它可以指示它们两个都拥有值并且彼此一致。自从两个TemperatureInput组件的props都来自于同一个父亲Calculator组件,那么这两个输入会一直保持同步。

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

首先,在temperatureInput组件里,我们会用this.props.value替换this.state.value。目前,让我们假装this.props.value已经存在了,虽然我们在后面将需要把它从Calculator里传递过来:

render() {
// Before: const value = this.state.value;
const value = this.props.value;

我们知道props是只读的。当value在state里,TemperatureInput能够调用this.setState()来改变它。然而,现在value是从父组件传递来并且作为一个prop,TemperatureInput组件无法控制它。

在React里,这样的情况经常通过使组件“受控”来解决。就像<input>DOM元素同时接受一个value和一个onChange属性,因此自定义的TemperatureInput也能从它的父组件Calculator那里同时接受value和onChange。

现在,当TemperatureInput想要更新温度,它就会调用this.props.onChange:

handleChange(e) {
// Before: this.setState({value: e.target.value});
this.props.onChange(e.target.value);

注意自定义组件里的value和onChange属性都没有特殊的含义。虽然这是一个共同的约定,但是我们可以给它们取任意名字。

onChange属性和value属性会一起通过父组件Calculator提供。它将会处理修改它自己的本地state的变化,从而使用新的值重新渲染两个输入。我们将很快看到Calculator的完成。

在深入Calculator组件里的变化之前,让我们先简要重述一下Temperature组件的变化。我们从本地state里移除了它,并且现在读取this.props.value而不是this.state.value。当我们要改变的时候,我们不调用this.setState(),而是调用Calsulator组件提供的this.props.onChange():

class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
} handleChange(e) {
this.props.onChange(e.target.value);
} render() {
const value = this.props.value;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={value}
onChange={this.handleChange} />
</fieldset>
);
}
}

现在让我们看一看Calculator组件。

我们将要保存当前输入的value和scale在它的state里。这是我们从输入那里提升的state,并且它会像真正源一样为两种输入服务。这是所有我们需要了解的为了渲染两个输入的数据的最小限度的表示。

举个例子,如果我们输入37到摄氏度输入框里,Calculator组件的state会像这样:

{
value: '37',
scale: 'c'
}
如果我们稍后编辑华氏温度框为212,Calculator的state会是这样:
{
value: '212',
scale: 'f'
}

我们本应该保存两个输入但是结果发现这不重要了。保存最近改变的输入和它代表的比例单位已经足够了。然后我们可以根据当前的value和scale来推断出另外一个。

这个输入保留在同步因为他们的值是通过同一个state计算出来的:

class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {value: '', scale: 'c'};
} handleCelsiusChange(value) {
this.setState({scale: 'c', value});
} handleFahrenheitChange(value) {
this.setState({scale: 'f', value});
} render() {
const scale = this.state.scale;
const value = this.state.value;
const celsius = scale === 'f' ? tryConvert(value, toCelsius) : value;
const fahrenheit = scale === 'c' ? tryConvert(value, toFahrenheit) : value; return (
<div>
<TemperatureInput
scale="c"
value={celsius}
onChange={this.handleCelsiusChange} />
<TemperatureInput
scale="f"
value={fahrenheit}
onChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}

在CodePen里试一试

现在,不论你编辑哪一个输入框,Calculator组件里的this.state.value和this.state.scale都会更新。其中获得value的输入保持现状,因此所有用户的输入都被保存,然后另外一个输入的值由已输入的值计算出。

我们简述当编辑一个输入框的时候会发生什么:

  • React在DOM元素<input>上调用指定的onChange函数。在我们这个例子下,调用的是TemperatureInput组件的handleChange方法。
  • TemperatureInput组件的handleChange方法调用this.props.onChange()带着新的value。它的属性,包括onChange都是父组件Calculator提供的。
  • 当它先前渲染的时候,Calculator组件指定摄氏度TemperatureInput组件的onChange就是Calculator组件的handleCelsiusChange方法,并且华氏度TemperatureInput组件的onChange就是Calculator的handleFahrehnheitChange方法。因此这两个Calculator方法被调用取决于哪一个输入框我们编辑了。
  • 在这些方法内部,Calculator组件会要求React去重新渲染它自己通过调用this.setState(),传递的值是新输入的温度和当前的比例单位。
  • React调用Calculator组件的render方法得知UI应该是什么样子。两个输入框的值都依据当前温度和比例单位重新计算。温度转换就在这里完成。
  • React调用单独的TemperatureInput的render方法,传递Calculator指定的新props。它会得知UI的样子。
  • React DOM更新UI来匹配输入的值。我们编辑的值会保持原状,另外一个值会通过计算得出,每一个更新都经过同样的步骤所以输入会彼此同步。

学习总结

在一个React应用里应该有一个“单一数据源”来应对任何改变的数据。通常,state是首先被添加到组件里需要它来处理渲染。接着,如果其他组件也需要state,你可以把它提升到最近的共同的祖先组件。想要在不同的组件之间同步state,你需要依赖从上到下的数据流。

将state提升会牵扯到写更多的“引用”代码,相比双向绑定要多得多。但是作为一个好处就是,这样子可以更快的找到和隔离程序中的bug。因为哪个组件保有状态数据,也只有它自己能够操作这些数据,发生bug的范围就被大大地减小了。此外,你可以完成任何自定义的逻辑去丢弃或者转变用户输入。

如果有东西可以既从props也可以从state里获取到,那么它可能不应该出现在state里。举个例子,我们只保存下最后一次编辑的value和scale,而不是去保存celsiusValue和fahrenheitValue的值。另一个输入框的值可以在render()方法里通过计算算出。这样我们可以清除或者运用四舍五入为其他输入域而不丢失精确度。

当你看到UI里有某些东西发生错误了,你可以使用React开发者工具去检查props并且顺着组件树寻找直到你找到更新state的组件。这样你可以跟踪bug到它们的源头。

React文档(十一)提升state的更多相关文章

  1. React文档(十三)思考React

    在我们的看来,React是使用js创建大型快速网站应用的首要方法.它在Facebook和Instagram的使用已经为我们展现了它自己. React的一个很好的地方就在于当你创建应用的时候它使你思考如 ...

  2. React文档(二十四)高阶组件

    高阶组件(HOC)是React里的高级技术为了应对重用组件的逻辑.HOCs本质上不是React API的一部分.它是从React的组合性质中显露出来的模式. 具体来说,一个高阶组件就是一个获取一个组件 ...

  3. react文档demo实现输入展示搜索结果列表

    文档页面地址:https://doc.react-china.org/docs/thinking-in-react.html 该文档只给了具体实现思路,下面是我实现的代码. 初学react,如果有写的 ...

  4. React文档(十六)refs和DOM

    Refs 提供了一种方式,用于访问在 render 方法中创建的 DOM 节点或 React 元素. 在标准的React数据流中,props是使得父组件和子组件之间交互的唯一方式.你通过props重新 ...

  5. React文档(一)安装

    React是一个灵活的可以用于各种不同项目的框架,你可以用它来写新应用,你也可以逐步将它引进已有的代码库而不用重写整个项目. 试用React 如果你想玩一玩React,那么就去CodePen上试一试. ...

  6. React文档总结

    元素渲染 更新元素渲染 计时器例子 function tick(){ const element = ( <div> <h1>Hello, World!</h1> ...

  7. React文档(七)处理事件

    React元素处理事件和DOM元素处理事件很类似.下面是一些语法的不同之处: React事件的命名是用驼峰命名,而不是小写字母. 利用JSX你传递一个函数作为事件处理器,而不是一个字符串. 举个例子, ...

  8. createDocumentFragment 文档碎片提升dom增删的性能

    原理: 操作dom会使得页面进行重新渲染,如果 经常性的对dom就行操作或者一次性操作dom较多,每一次操作都会使页面进行重新渲染,降低页面加载性能. 针对IE9以下,可以使用文档碎片(documen ...

  9. elasticsearch 父子文档(十一)

    说明 需求 一个产品多个区域销售 每个区域有自己的价格, 方式1冗余行,a 产品分别在  area1 area2 area3区域销售 a产品就会生成3条产品数据 搜索id去重就行了,但是问题就是 聚合 ...

随机推荐

  1. h5新增标签及css3新增属性

    - h5新增的标签 新增元素 说明 video 表示一段视频并提供播放的用户界面 audio 表示音频 canvas 表示位图区域 source 为video和audio提供数据源 track 为vi ...

  2. 5.0-uC/OS-III时间管理

    1.时间管理 uC/OS-III为用户提供了与时间管理相关的服务. 在uC/OS-III中设置了能提供时基中断的中断源.该中断源提供 10Hz 到 1000Hz 之间的中断(需设置OS_CFG_APP ...

  3. springboot中通过cors协议解决跨域问题

    1.对于前后端分离的项目来说,如果前端项目与后端项目部署在两个不同的域下,那么势必会引起跨域问题的出现. 针对跨域问题,我们可能第一个想到的解决方案就是jsonp,并且以前处理跨域问题我基本也是这么处 ...

  4. Vuex之理解Getters的用法

    一.什么是getters在介绍state中我们了解到,在Store仓库里,state就是用来存放数据,若是对数据进行处理输出,比如数据要过滤,一般我们可以写到computed中.但是如果很多组件都使用 ...

  5. git中设置http代理和取消http代理

    设置http代理 git config --global https.proxy https://127.0.0.1:1080 取消http代理git config --global --unset ...

  6. ARIMA模型总结

    时间序列建模基本步骤 获取被观测系统时间序列数据: 对数据绘图,观测是否为平稳时间序列:对于非平稳时间序列要先进行d阶差分运算,化为平稳时间序列: 经过第二步处理,已经得到平稳时间序列.要对平稳时间序 ...

  7. cocos2d-x JS 各类点、圆、矩形之间的简单碰撞检测

    这里总结了一下点.圆.矩形之间的简单碰撞检测算法 (ps:矩形不包括旋转状态) 点和圆的碰撞检测: 1.计算点和圆心的距离 2.判断点与圆心的距离是否小于圆的半 isCollision: functi ...

  8. Oracle中如何使用REGEXP_SUBSTR函数动态截取字符串

    REGEXP_SUBSTR函数格式如下: function REGEXP_SUBSTR(String, pattern, position, occurrence, modifier)__srcstr ...

  9. TP无限回复

    引入文件和css样式 <script src="__PUBLIC__/bootstrap/js/jquery-1.11.2.min.js"></script> ...

  10. 把vim插入状态的光标改为竖线

    和终端有关系,如果是Konsole的终端,把下面两行加到.vimrc文件里就可以 let &t_SI = "\<Esc>]50;CursorShape=1\x7" ...