React核心概念:状态提升

2023-11-20

上一节:表单
下一节:组合vs继承

引言

很多情况下我们使用的多个组件需要对同一个数据做出对应的反应。在这里我们推荐把这个共享的状态提升到距离这些组件最近的祖先组件。现在让我们来看看这是怎么工作的。

在本章中,我们将会创建一个温度计算器来计算在给定温度下水是否会沸腾。

首先我们现创建一个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组件。它渲染了一个输入框来输入温度并将输入值绑定到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>
    );
  }
}

添加第二个输入框

现在我们有一个新需求,除了输入摄氏温度之外,我们还需要一个能够输入华氏温度的输入框,并且这两个输入框是同步的。

首先我们可以从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 = {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>
    );
  }
}

现在我们有了两个输入框,但是当你在其中一个输入时另一个输入框内的数据并不会更新。这就与我们想要这两个输入框同步的需求矛盾了。

我们也没法在Calculator中展示BoilingVerdict了,因为Calculator组件没有办法获取到隐藏在TemperatureInput组件里的温度。

编写转换函数

为了解决上述的矛盾,我们现编写能够相互转换摄氏度和华氏温度的两个函数。

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

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

这两个函数用来转换数字,现在我们来编写另一个函数,这个函数讲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组件都将输入值保存在各自的局部state中。

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;
    // ...  

但是我们想要这两个输入框能够同步数据,当我们在摄氏度输入时,华氏温度能够同时显示经过转化后的温度,反之亦然。

在React中,共享状态的方法是将state转移到离需要共享组件最近的公共父组件中,这称为“状态提升”。现在我们将把TemperatureInput组件中的state转移到Calculator中。

如果Calculator包含了共享状态,那么它就是这两个温度输入组件的“数据源”,这能使两个输入组件的数据始终保持一致。 因为两个TemperatureInput组件的props都是来自共同的父组件Calculator的,所以他们在数据显示上能够保持同步。

现在我们来逐步了解这是怎么完成的。

首先,我们将TemperatureInput组件中的this.state.temperture替换成this.props.temperature。当然this.props.temperature是通过Calculator组件传递的。

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

我们知道props是只读的,当我们将温度存储在本地state时,我们可以通过setState()来修改它。但是现在温度是通过父组件传递进来的,那么TemperatureInput组件就没有修改温度的权限了。

React中,通常的解决方案是将组件变为“受控”的。就像在DOM中<input>接收valueonChange作为prop一样,我们可以让TemperatureInput组件接收从Calculator传递来的prop:temperatureonTemperatureChange

现在,当TemperatureInput想要更新温度时,只要调用this.props.onTemperatureChange就行了:

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

提示
temperature和onTemperatureChange在这里没有特殊的含义,只是一个属性名称,是可以随意定义的,它也可以是更大众化的value和onChange。

onTermperatureChange属性和temperature属性都是由父组件Calculator提供的。它会通过修改自身的state来处理数据的变化,以此来重新渲染两个输入框内的数据。我们很快就可以看到Calculator组件的实现细节。

在我们深入了解Calculaor组件的实现之前,先让我们来回顾一下TemperatureInput组件做了哪些修改。我们将移除了本地state,将原本通过读取this.state.temperature获取温度替换成读取this.props.temperature获取,将通过调用setState()修改数据替换成调用this.props.onTemperatureChange()修改数据,这两种都由Calculator组件提供。

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组件。

我们将输入框的temperature和scale存储在本地state中,这是我们从input中提升出来的状态,来作为两个input的数据源。这就是我们渲染两个input需要的最小数据集合。

举个例子,现在我们在摄氏度输入框内输入37,那么Calculator组件的state就将会是这样:

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

如果之后我们在华氏温度输入框中输入212,Calculator的state就将变成:

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

有人会说为什么存储两个input的值?可以,但没必要。只要存储最近修改的值和它所代表的scale就足够了。因为我们可以根据当前的temperature和scale推算出另一个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>
    );
  }
}

现在,无论你在哪个输入框输入,Calculator组件的this.state.scalethis.state.temperature都会更新。其中一个输入框的输入按照原样取值,于是用户的输入就这样被保存了,而另一个输入框的值就会根据用户的输入推算出来。

让我们梳理以下当我们在一个输入框输入时代码的运行逻辑:

  • React调用DOM标签<input>上的onChange方法,在我们的例子中,调用的时TemperatureInput组件的handleChange方法。
  • TemperatureInput组件中的handleChange被调用时,它将最新的数据传递给this.props.onTemperatureChange()。这里的props都由父组件Calculator提供。
  • 之前渲染的时候,Calculator组件在摄氏度的TemperatureInput组件上声明的onTemperatureChange事件所调用的方法是handleCelsiusChange,在华氏温度上的则是handleFahrenheitChange。所以这两个函数的调用取决于我们在哪个输入框输入数据。
  • 在这些方法中,Calculator组件调用setState()将最新输入的值和scale重新渲染。
  • React调用Calculator组件的render()方法将UI渲染在页面上。两个输入框的值都在此时根据当前的状态重新计算,温度的换算也在此时进行。
  • React调用两个TemperatureInput组件的render()方法,并根据由Calculator定义的props渲染在页面上。
  • React调用BoilingVerdict组件的render()方法,并根据传进来的摄氏度温度渲染数据。
  • React DOM更新沸腾结果和输入框的内容。我们刚刚编辑的输入框接收当前的值,而另一个输入框接收经过转换的值。

每一次更新都按照上述步骤进行,所以两个输入框数据可以保持同步。

经验总结

在React应用中任何可变的数据都应该有唯一数据源。通常来说,state一开始都是包含在需要根据它来渲染数据的组件中。但是如果有其他组件也想要使用这个state,那么就需要把它提升到距离这两个组件最近的公共父组件中。但是相比于保持不同组件的数据同步,我们更应该依靠的是自顶而下的数据流。

相较于双向绑定,状态提升需要编写更多的“样板”代码。但这样做的好处是我们可以更好地定位和分离bug。因为state只存在于各自的组件中,所以bug出现范围就大大减少了。除此之外,我们可以自由地实现任何逻辑来拒绝或修改用户输入。

如果某个数据可以根据state或者props推导出来,那么这个数据就不应该存储在state中。就像本章的实例代码中,我们只存储最新输入的温度和它对应的scale,并不需要将华氏温度和摄氏温度都存储在state中。另一个输入框的数据在调用render()方法时就可以根据当前的数据推算出来。这能让我们清除用户输入或者在不损失用户输入精度的情况下在其他区域使用用户输入的值。

当页面出现错误时,你可以使用React Developer Tools查看props并且在组件树上追溯源头直到找到对应的组件为止。这能够让你轻松地定位bug。
在这里插入图片描述
上一节:表单
下一节:组合vs继承

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

React核心概念:状态提升 的相关文章

  • Javascript 与 Python 关于 Python 'map()' 函数的比较

    Python中有一个函数叫做map这可以让你去 map someFunction x y z 并继续应用该功能的列表 是否有与此功能等效的 JavaScript 我现在刚刚学习Python 虽然我被告知javascript是函数式语言 但我
  • 如何在光标下的所有元素上调用 mouseover?

    我有一个网络应用程序 每次单击时都会创建一个点 见下文 当我将鼠标悬停在一堆点上时 我希望光标下的每个点都会触发 mouseover 或 mouseenter 事件 然而 只有一个事件被触发 即堆栈 顶部 的点的事件 当鼠标移动到一堆多个点
  • 如何使用 LinkedIn javascript sdk 检索包括所有字段的职位列表?

    我想要获取 LinkedIn 会员在其个人资料中输入的每个职位的 ID 头衔 摘要 开始日期 结束日期 当前状态和公司名称 我测试了一个查询休息控制台 https apigee com console linkedin我得到了想要的结果 查
  • 如何根据按钮单击折叠和展开 Kendo UI 树视图中的所有树节点?

    这是行不通的 您可以使用此代码 1 崩溃 折叠kendoTree查看文档 http docs kendoui com api web treeview methods collapse treeview kendoTreeView var
  • 未捕获的类型错误:this.props.signinUser 不是一个函数(…)

    src actions index js import axios from axios const ROOT URL http localhost 3090 export function signinUser email passwor
  • 如何从表中选择所有偶数 id?

    我想从 MySQL 数据库的表中选择所有甚至帖子 ID 然后显示它们 我还想获取所有带有奇怪 id 的帖子并将它们显示在其他地方 我想使用 PHP 来完成此操作 因为这是我使用的服务器端语言 或者 我是否必须选择所有帖子 然后使用 Java
  • 零作为 IIFE 中的第一个参数[重复]

    这个问题在这里已经有答案了 In babeljs v6 5 1 class Foo 编译为 use strict var classCallCheck2 require babel runtime helpers classCallChec
  • JavaScript 变量赋值与 OR 对比 if 检查[重复]

    这个问题在这里已经有答案了 在 JavaScript 中 我最近意识到你可以使用 OR 赋值的逻辑运算符 我想知道这是否被认为是不好的做法 特别是 我有一些具有可选数组输入的函数 如果输入是null or undefined我应该将它设置为
  • jQuery 的 css() 在应用于滚动事件时滞后

    我正在尝试实现一个简单的 固定标题 表 我知道这在理论上只能用 CSS 来完成 但是当涉及到 OSX Lion 及其消失的滚动条时 它效果不佳 所以我用 jQuery 来做 方法很简单 只有1 5行代码 inbox scroll funct
  • 从未定义解构时避免错误

    可以说我有这个代码 const x y point Babel 会将其变成 var point point x point x y point y 这很好 但是如果点未定义怎么办 现在我得到一个错误 Cannot read property
  • ReactJS:从没有 onChange 事件的 props 中选择默认值?

    所以 我只是想知道我是否绝对需要在 React 中的选择组件上有一个 onChange 事件处理程序 我有一个道具传递我想要选择的选项的默认值 如果我有的话 它就没有问题
  • 空 URL 哈希导致页面在 js 事件上跳转

    我有一个带有下一个和上一个按钮的照片库 如果我的某个 javascript 方法由于某种原因被破坏 那么当单击其中一个按钮时 它会向 url 添加一个哈希值 即 www google com 我知道可以给散列一个 div id 来跳转到页面
  • setInterval 内的返回值

    我想在 setInterval 内返回一个值 我只想以一定的时间间隔执行一些操作 这就是我尝试过的 function git limit var i 0 var git setInterval function console log i
  • Web SQL 数据库 + Javascript 循环

    我正在尝试解决这个问题 但我自己似乎无法解决 我正在使用 Web SQL DB 但无法让循环正常使用它 I use for var i 0 i lt numberofArticles 1 i db transaction function
  • 使用 JavaScript 的计时器

    我想使用java脚本实现计时器 我想随着间隔的变化而减少计时器 Example假设我的计时器从 500 开始 我想要根据级别减少计时器 例如1 一级定时器应减1 且递减速度应较慢 2 2级定时器应递减2 递减速度应为中等3 3级定时器应减3
  • Meteor.js 登录事件

    因此 我对 Meteor 框架和 JavaScript 总体来说还很陌生 但我正在使用该框架开发一个小项目 以尝试让自己达到标准 基本上我正在开发一个微博客网站 目前 用户可以通过多种服务登录 fb google 等 我通过插入所需 url
  • 使用 jQuery Tablesorter 操作后如何恢复当前页面?

    我正在使用 tablesorter 但无法找到有关插件 tablesorter 寻呼机的任何文档 问题是我有一个显示一些数据的表 并且在每一行中都有一个删除链接 该链接附加了要删除的元素的唯一标识符 显然 是否可以保存我正在删除的页面 然后
  • Dojo/on 和捕获阶段

    有没有办法用 dojo on 在捕获阶段 而不是冒泡阶段 触发事件 我最终在这里寻找有关 on 的前身 dojo connect 的信息 就其价值而言 dojo connect 似乎不支持捕获阶段的事件侦听器 它的工作原理是将事件处理程序作
  • 如何在 JavaScript 中获取浮点数的小数位?

    我想要的是与 Number prototype toPrecision 几乎相反的 这意味着当我有数字时 它有多少位小数 例如 12 3456 getDecimals 4 对于任何想知道如何更快地完成此操作 无需转换为字符串 的人 这里有一
  • Serviceworker Bug event.respondWith

    我的 serviceworker 的逻辑是 当发生获取事件时 它首先获取包含一些布尔值 而不是 event request url 的端点 并根据我正在调用的值检查该值event respondWith 对于当前的获取事件 我正在提供来自缓

随机推荐

  • 【vue】前端下载文件自定义文件名称

    下载文件自定义文件名称 文件下载名称不想和后端提供的URL一样怎么办呢 1 首先给按钮去绑定一个事件 2 正常我们的下载处理方式 3 自定义下载的文件名字 文件下载名称不想和后端提供的URL一样怎么办呢 1 首先给按钮去绑定一个事件 按钮的
  • 微信小程序第六篇:元素吸顶效果实现

    系列文章传送门 微信小程序第一篇 自定义组件详解 微信小程序第二篇 七种主流通信方法详解 微信小程序第三篇 获取页面节点信息 微信小程序第四篇 生成图片并保存到手机相册 微信小程序第五篇 页面弹出效果及共享元素动画 话不多说 先看效果 这种
  • ElasticSearch ected map for property [fields] on field [subject_id] but got a class java.lang

    ElasticSearch的聚类时出现fielddata true Expected map for property fields on field subject id but got a class java lang String
  • 前端导出多级表头

    前端导出多级表头 今天在技术交流群里面看到有人问到了这一块 我之前看过一些关于这样的代码 我就直接给他上了代码 自己又重新练习里一遍 这是结合elementUI来写的一个表格 先看一下练习的是这样的效果 首先还是要安装依赖 npm inst
  • Howto Upgrade Debian

    Howto Upgrade Debian 4 Etch to Debian 5 0 Lenny HowTo Upgrade Debian 5 0 Lenny To Debian 6 0 Squeeze HowTo Upgrade Debia
  • Centos7虚拟机创建并设置静态IP(桥接模式)

    一 准备工作 1 Centos7镜像文件下载 下载地址 Centos7下载地址 2 VMware安装 下载地址 VMware下载地址 二 创建虚拟机 1 新建虚拟机 新建虚拟机 选择典型即可 选择刚才下载的ISO镜像 虚拟机命名 可以修改虚
  • Flask项目部署到Ubuntu上

    前期准备 将在本地开发好的Flask项目打包 发送到云主机上 可以使用xftp等传输工具放到远程主机上 安装python虚拟环境 为python3安装pip sudo apt install python3 pip 为python安装pip
  • PicoDet的学习笔记

    学习资源 Paddle官方教程 AI快车道PaddleDetection 课节4 闪电版目标检测算法PP PicoDet PicoDet增强版官方介绍 超强目标检测算法矩阵 PicoDet XS PicoDet论文 PP PicoDet A
  • eclipse调试的时候进入了class文件

    解决方法是右键工程 debug as gt debug configuration 找到Apach Tomcat 点击source 将default 文件夹删除 然后点击Add 添加本地的 java project 项目即可
  • 开发APP前期的准备工作到底有多重要??

    如果经历过一个app从零开发的同学 可能就会知道 app前期的打地基到底有多重要 我从开始工作到现在 目前都是从零开发app的 没有试过中途填别人的坑 但是我试过留着泪给自己填坑 还是那种自己都不知道自己为什么要这样写的坑 改了还会崩溃 这
  • IDEA2020mybatis错误:Error:(5, 28) java: 程序包org.apache.ibatis.io不存在

    出现该错误是因为maven与IIDEA新版本不兼容 勾选IDEA默认集成maven 即修改一maven home directory为Bundled Maven 3 并修改为IDEA2020默认给的路径 不要用自己的maven本地仓库的路径
  • 2015物联网产业群雄逐鹿 推进产业健康发展

    本文转载至 http www 50cnnet com show 34 84566 1 html 核心提示 自2010 年 国务院关于加快培育和发展战略性新兴产业的决定 首次将物联网产业列为国家发展战略后 国家及各省市又陆续出台了针对涉及物联
  • # flutter、rn、uni-app比较

    flutter rn uni app比较 更新 DCloud已推出强大的uts 虽然第一个版本还不适于开发ui 但会陆续升级 这将是最佳的跨平台解决方案 详见 前言 每当我们评估新技术时要问的第一个问题就是 它会给我们的业务和客户带来哪些价
  • 闯关游戏 xss-lab

    level 1 直接测试 我们看到参数是name 那就吧payload带进去 发现直接就注入了 没有任何的过滤 其实我们可以先构造一串敏感字符 用作测试 看看程序是如何过滤的 比如
  • 基于SSM的校园二手交易平台

    一 源码获取 链接点击直达 下载链接 二 系统架构 使用技术 Spring SpringMVC Mybatis 三 系统需求分析 在如今的大学校园中 伴随着学生的购买能力的提高和每年的升学和毕业 存在许多各种类型的二手商品 目前 二手商品交
  • 安全顶刊论文阅读总结1

    论文阅读总结 An Explainable AI Based Intrusion Detection System for DNS Over HTTPS DoH Attacks 论文介绍 本文2022年发表在IEEE Transaction
  • Java 比较器 -- 对象比较

    基本数据类型比较大小时 我们可以用比较运算符 当两个对象比较大小时 我们就可以用比较器了 实现的方式有两种 如下 方式一 自然排序 实现接口Comparable 创建一个自定义类Students 实现接口Comparable 并重写comp
  • TensorFlow在Win10上的安装教程和简单示例

    安装说明 平台 目前可在Ubuntu Mac OS Windows上安装 版本 提供gpu版本 cpu版本 安装方式 pip方式 Anaconda方式 Tips 在Windows上目前支持python3 5 x gpu版本需要cuda8 c
  • Linux下gdb调试生成core文件并调试core文件

    1 什么是core文件 有问题的程序运行后 产生 段错误 核心已转储 时生成的具有堆栈信息和调试信息的文件 编译时需要加 g 选项使程序生成调试信息 gcc g core test c o core test 2 怎样配置生成 core 文
  • React核心概念:状态提升

    上一节 表单 下一节 组合vs继承 React核心概念 状态提升 引言 添加第二个输入框 编写转换函数 状态提升 经验总结 引言 很多情况下我们使用的多个组件需要对同一个数据做出对应的反应 在这里我们推荐把这个共享的状态提升到距离这些组件最