提升状态

通常,几个组件需要根据同一个数据变化做出响应。我们建议将这个共享的状态提升到他们最近的一个共用祖先。让我们看看实际该怎么做。
在这一节,我们将创建一个温度计算器,用来计算一给定温度能否让水沸腾。
我们从名为BoilingVerdict的组件开始。它接受celsius温度作为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.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上试一试

添加第二个输入

我们有个新的需求,除了一个摄氏度输入,我们还要提供一个华氏输入,并且他们保持同步。
我们先从Calculator中提取TemperatureInput组件。然后为其添加一个新的scaleprop,它的值值为"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 (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

现在我们可以改变Calculator来渲染两个独立的温度输入:

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

在CodePen上试一试
现在我们有两个输入了,但是当你在其中一个里输入温度,另一个不会去更新。这就不满足我们的需求了,我们想让他们同步。
我们也没在Calculator中显示BoilingVerdictCalculator不知道当前的温度,因为温度被隐藏在TemperatureInput中。

编写转换函数

首先我们写两个函数来互相转换摄氏度和华氏温度。

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

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

这两个函数转换数字。接下来我们写另一个函数,接受一个temperature字符串和一个转换函数作为参数,返回一个字符串。我们将用他来根据另一个input来计算这个input的值。
一个无效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;

然而,我们希望这两个input可以彼此同步。当我们更新摄氏度input,华氏度input也会相应的转换温度,反之亦然。
在React中,想要共享状态,就需要找到共享状态组件的一个最近共有祖先,然后通过将该状态移动到这个共有祖先上来完成共享。这称为“提升状态”。我们先移除TemperatureInput的本地状态,取而代之的是将它移到Calculator中。
如果Calculator拥有共享状态,对于两个input中的温度来说他就成为了“真相的来源”。他就可以指示两个input具有相同的值。由于两个TemperatureInput组件的props都来自同一个父组件Calculator,两个input将始终保持同步。
让我们逐步分析这是如何工作的。
首先,在TemperatureInput组件中,我们使用this.props.temperaturethis.state.temperature替换掉。现在,让我们假设this.props.temperature已经存在,之后我们会从Calculator中传入该值:

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

我们知道props是只读的。之前temperature在本地状态中,TemperatureInput只能调用this.setState()来改变它。而现在,temperature作为prop从父组件获取,TemperatureInput不能再控制它了。
在React中,一般通过将组件变为“受控”,来解决这个。就像DOM<input>接受一个value和一个onChangeprop,所以自定义的TemperatureInput可以从它的父组件Calculator中获取temperatureonTemperatureChangeprops。
现在,当TemperatureInput想要更新它的温度值,调用this.props.onTemperatureChange就好了:

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

注意,自定义组件中的prop名字:temperatureonTemperatureChange并没什么特别的意思。我们可以随意命名,比如给它们更通用的名字valueonChange
父组件Calculator提供proponTemperatureChange的同时也提供temperature。他通过修改自己的本地状态来处理更改,从而将两个input重新渲染为新的值。我们马上就来看看新的Calculator实现。
在深入Calculator的变化之前,我们来重新看遍TemperatureInput组件中做过什么变动。我们将他的本地状态移除,将从this.state.temperature读取,改为从this.props.temperature读取。当我们想做出变化时,现在我们调用Calculator提供的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 (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

现在让我们回到Calculator组件。
我们将当前输入的temperaturescale存到他的本地状态。这就是我们从input中“提升”的状态,该状态将作为“真相的来源”提供给两个TemperatureInput。这是我们渲染两个input,所需数据的最少表示。
假如,我们在摄氏度输入框中输入37,Calculator组件的状态如下:

{
  temperature: '37',
  scale: 'c'
}

如果我们将华氏字段编辑为212,Calculator的状态将变为:

{
  temperature: '212', 
  scale: 'f'
}

我们可以存储两个输入的值,但实际上是不必的。存储最后一次变化的值和单位即可。然后我们可以根据当前的温度和单位来换算出另一个值。
因为两个input的值从同一个状态计算而来,所以他们始终保持同步:

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 (
      <div>
        <TemperatureInput
          scale="c"
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange} />
        <TemperatureInput
          scale="f"
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange} />
        <BoilingVerdict
          celsius={parseFloat(celsius)} />
      </div>
    );
  }
}

在CodePen上试一试
现在,不论你编辑哪一个input,Calculator中的this.state.temperaturethis.state.scale都会得到更新。从任何一个input获取的用户输入都会被保存,另一个input的值会根据它重新计算。
让我们重新看下当你编辑一个input时发生了什么:

  • React调用DOM<input>上指定为onChange的函数。在我们的例子中这个函数是TemperatureInput组件的handleChange方法。
  • TemperatureInput中的handleChange方法,使用新的需求值调用this.props.onTemperatureChange()。他的props,包括onTemperatureChange,都是由他的父组件Calculator提供。
  • 在渲染之前,Calculator已经将自己的handleCelsiusChange方法赋值给摄氏度TemperatureInputonTemperatureChange,还将自己的handleFaherenheitChange方法赋值给华氏度TemperatureInputonTemperatureChange。所以,Calculator的这个两个方法会根据我们编辑的input,而得到调用。
  • 在这些方法中,Calculator组件通过使用新的输入值和在编辑中input的单位来调用this.setState(),使得React重新渲染自己。
  • React通过调用Calculator组件的render方法来获取UI的外观。两个input的值根据当前的温度和单位重新计算。温度的换算在这个时候进行。
  • React根据Calculator提供的新props来分别调用TemperatureInputrender方法。由此得知他们UI的外观。
  • React DOM更新DOM来匹配所需的input值。我们编辑的input接受当前的值,另一个input更新为转换后的问题。

每次更新都会重复上面的步骤,从而使所有input保持同步。

经验教训

在React应用中,所有变化的数据都应该是单独的“真相来源”。通常,状态第一个被添加到组件中(组件需要用这些状态来进行渲染)。如果其他组件也需要它,你可以将状态提升到这些组件共用的最近祖先。你应该依赖自上而下的数据流,而不是同步多个组件的状态。
比起双向绑定的方法,提升状态需要写更多的“样板”代码,但好处就是,它可以更轻松的找到和隔离bug。因为任何状态都是存在于组件中,并且只有组件可以修改它,由此bugs存在的区域大大被减少。另外,你可以实现任意逻辑来拒绝或转换用户的输入。
如果某个值可以通过其他props或状态获得,那他就不该把他放在状态中。比如,我们仅仅存储上一次编辑的温度和单位,而不是将celsiusValuefahrenheitValue都存下来。因为在render()方法中,一个input的值始终可以通过另一个计算得来。这样,我们对另一个字段用或不用四舍五入,都不会在用户的输入中丢失精度。
当你发现UI上有错误发生,你可以使用React开发者工具来检查props,然后沿着树结构向上,知道你找到负责更新状态的组件。这使你追溯到bug的来源:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容

  • 最近看了一本关于学习方法论的书,强调了记笔记和坚持的重要性。这几天也刚好在学习React,所以我打算每天坚持一篇R...
    gaoer1938阅读 1,662评论 0 5
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,559评论 18 139
  • 昨晚熬夜抑或加班,一大早到公司难免精神疲惫,哈欠连连。此时,有的人开始无所事事四处闲逛,有的人趴在桌上玩着手机,有...
    冯凯源阅读 712评论 0 0
  • React版本:15.4.2**翻译:xiyoki ** 通常几个组件需要响应相同的数据变化。我们建议将共享状态提...
    前端xiyoki阅读 660评论 0 0
  • 这篇笔记主要包含 Vue 2 不同于 Vue 1 或者特有的内容,还有我对于 Vue 1.0 印象不深的内容。关于...
    云之外阅读 5,043评论 0 29