9. Lifting State Up(状态提升)

React版本:15.4.2
**翻译:xiyoki **

通常几个组件需要响应相同的数据变化。我们建议将共享状态提升到最接近的共同祖先。让我们看看这是如何工作的。
在本节中,我们将创建一个温度计算器,用于计算水是否在给定温度下沸腾。
我们将从名为BoilingVerdict的组件开始。它接受celsius温度为props,并打印是否足以将水烧开:

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

Adding a Second Input(增加第二个输入)

我们新的要求是:除了摄氏度输入框,我们还提供华氏度输入框,并且二者是同步的。
我们把从Calculator中提取一个TemperatureInput组件作为开始。我们将为它增加一个新scale prop,并且这个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>
    );
  }
}

现在我们有两个输入框,但当你向其中一个输入框输入温度时,另一个并不会更新。这违反了我们的要求:我们希望它们保持同步。
我们也不能从Calculator中展示BoilingVerdictCalculator也不知道当前的温度,因为当前温度被隐藏在了TemperatureInput内部。

Lifting State Up(状态提升)

首先,我们将写两个函数来将摄氏度转换为华氏度,将华氏度转换为摄氏度:

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

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

这两个函数转换数字。我们将写另一个函数,它接受一个字符串value和一个转换器函数作为参数,并返回一个字符串。我们将使用它来计算其中一个输入框的值,而该输入框的值基于另一个输入框。
它返回一个无效的空字符串value,并且它将输出四舍五入到三位小数:

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’
接下来,我们将从TemperatureInput中删除状态。
相反,TemperatureInput组件将接受valueonChange处理程序作为prop:

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。我们将在它的状态中存储当前的valuescale
我们可以存储两个输入的值,但事实证明这是不必要的。它足以存储最近被更改的输入框的值,以及其表示的scale。然后我们能基于当前的valuescale,单独推断其他输入框的值。
输入的值保存同步,因为它们的值从相同的状态计算而来。

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; {/* 对状态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>
    );
  }
}

不论你编辑哪个输入框,Calculator中的this.state.valuethis.state.scale都会获得更新。其中一个输入框获取的值为原样,因此任何用户的输入都被保留,另一个输入框中的值总是基于它重新计算。

Lessons Learned (得到的教训)

对于在React应用程序中更改的任何数据,应该有一个单一的‘真实来源’。通常,首先将状态添加到需要渲染的组件。然后,如果其他组件也需要它,你可以将其提升到最接近的共同祖先。而不是尝试在不同组件之间同步状态,你应该依赖于自上而下的数据流。
提升状态涉及编写比双向绑定方法更多的‘样板’代码。但好处是找到和隔离bug需要较少的工作。由于任何状态存在于特定的组件中,并且该组件可以单独改变它,所以大大减少了错误的表面积。此外,你可以实现任何自定义逻辑以拒绝或转换用户输入。
如果数据可以从props或state派生,那么它就不应该在状态之中。例如,我们只存储了最后编辑的valuescale,而不是存储两个celsiusValuefahrenheitValue。另一个输入的值总是可以从render()方法中计算出来。这允许我们清除或应用四舍五入到其他字段,而不会丢失用户输入的任何精度。
当你在UI中看到错误时,可以使用 React Developer Tools 检查props,并向上移动树,直到找到负责更新状态的组件。这使你可以跟踪错误到其来源:

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

推荐阅读更多精彩内容

  • 通常,几个组件需要根据同一个数据变化做出响应。我们建议将这个共享的状态提升到他们最近的一个共用祖先。让我们看看实际...
    莫铭阅读 890评论 0 1
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,451评论 25 707
  • 通常,一些组件需要对同一个数据做出反应,这时我们建议将这些组件中关于这个数据的State提升至和它们距离最近的父级...
    编码的哲哲阅读 427评论 0 0
  • 感恩这几天的好天气,因为天气好,我的心情也好很多!感恩周六周日孩子们的陪伴,以解相思之苦!虽说很忙碌但...
    心静感恩阅读 164评论 0 0
  • 大雾漫天风飞扬, 遮云蔽日在何方。 鸿雁传书思断肠, 魂牵梦绕归故乡。
    红嘴唇卍阅读 271评论 0 0