经常有些组件需要映射同一个改变的数据。我们建议将共用的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. UIScrollView上面的UIButton点击始终在中间

    -(void)btnClick:(IdleTopChoseBtn *)btn{ btn.selected = YES; _choseBtn.selected = NO; _choseBtn = btn ...

  2. python框架之Django(13)-admin组件

    使用 Django 提供了基于 web 的管理工具. Django 自动管理工具是 django.contrib 的一部分.你可以在项目的 settings.py 中的 INSTALLED_APPS ...

  3. NLP:Gensim库之word2vec

    Gensim是一款开源的第三方Python工具包,用于从原始的非结构化的文本中,无监督地学习到文本隐层的主题向量表达.它支持包括TF-IDF,LSA,LDA,和word2vec在内的多种主题模型算法, ...

  4. python set的函数

    1. add() 为集合添加元素 2. clear() 移除集合中的所有元素 3. copy() 拷贝一个集合 4. difference() 返回多个集合的差集 5. difference_upda ...

  5. JavaScript获取及判断文件类型

    一.获取文件后缀 <input type="file" name="addvedio" accept="video/*"/>注: ...

  6. Python之包管理

    1.setup.py from distutils.core import setup setup(name='Distutils', version='1.0', description='Pyth ...

  7. 【Spark-core学习之七】 Spark广播变量、累加器

    环境 虚拟机:VMware 10 Linux版本:CentOS-6.5-x86_64 客户端:Xshell4 FTP:Xftp4 jdk1.8 scala-2.10.4(依赖jdk1.8) spark ...

  8. Python 语言来编码和解码 JSON 对象

    Json函数: json.dumps: Python标准库中的json模块,集成了将数据序列化处理的功能. 将 Python 对象编码成 JSON 字符串 语法: json.dumps(obj, sk ...

  9. Linux基础命令---accept/reject 允许拒绝发送打印请求

    accept accept指令用来设置允许向目标打印机发送打印任务. 此命令的适用范围:RedHat.RHEL.Ubuntu.CentOS.Fedora.   1.语法      cupsaccept ...

  10. 第五讲 DOM基础

    DOM基础: 什么是DOM:其实就是dovument,div获取.修改样式等等,但是不只是js的组成部分,而且还是一套规范,规定了这些浏览器怎么处理这些操作: 浏览器支持情况:IE(IE7-8,10% ...