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核心概念:状态提升 的相关文章

  • Safari 中的日期无效

    alert new Date 2010 11 29 chrome ff 对此没有问题 但 safari 会喊 无效日期 为什么 编辑 好的 根据下面的评论 我使用了字符串解析并尝试了这个 alert new Date 11 29 2010
  • SSE (EventSource) 在 1 小时 22 分钟后超时。有什么办法可以让它持续下去吗?

    我的页面中有一个区域 当数据库发生更改时 消息将发送到该区域 现在 有时数据库会发生很大的变化 以至于每 10 分钟就会显示一条新消息 其他日子它只会改变几次 我遇到的问题是 EventSource 似乎在 1 小时 22 分钟后超时 浏览
  • 如何在不污染全局命名空间的情况下公开 javascript 对象以进行单元测试

    我有一个 javascript 自动完成插件 它使用以下类 用咖啡脚本编写 查询 建议 建议集合和自动完成 每个类都有一个用 Jasmine 编写的相关规范 该插件在模块中定义 例如 function plugin call this 这可
  • 我可以在 chrome devtools 中执行 nodejs javascript 脚本吗?

    是否可以使用Chrome 开发工具执行终端命令node myfile js 所以chrome控制台会输出所有console log来自我的代码 我有一些terminal插件安装在我的 IDE 中 当我想在我的文件上运行此命令时 我使用一些键
  • 将 showModalDialog() 的内容添加到剪贴板 Google 脚本

    当我单击按钮时 我已将格式化数据添加到模态对话框中 我想要的内容showModalDialog 当我单击按钮时也会自动添加到剪贴板 模态是用下面的代码生成的 并且temp是我想要添加到剪贴板的输出 Output to Html var ht
  • 如何以 Express 方式传输响应?

    我一直在尝试让一个快速应用程序以流形式发送响应 var Readable require stream Readable var rs Readable app get report function req res res statusC
  • Socket IO聊天例子很慢

    我是 Node js 和 Socket IO 的新手 我想尝试一下解释的示例 https socket io get started chat https socket io get started chat 我做了我必须做的一切 它起作用
  • 带层的 NodeJS Lambda - 如何防止打字稿在构建中包含层

    我正在用打字稿编写一个 NodeJs lambda 函数 它依赖于 Lambda 层才能工作 我在项目文件夹中创建了一个包含示例层的目录 然后使用 tsconfig 文件中的路径选项以便在本地测试它 然而 这工作得很好 当我构建代码来编译
  • angularjs ng-grid:行之间的父子关系(隐藏/显示行)

    我正在尝试使用预定义的隐藏行来实现渲染 ng grid 并在某些特定事件上我想显示它们 我试图模拟行之间的父子关系 但所有行都将以通常的方式呈现和放置 默认情况下 子 行将呈现为 折叠 单击父项时 子行将显示为展开的 我正在尝试寻找一些解决
  • 三.JS Shadow 到对象

    我想添加castShadow and receiveShadow在一个物体上 但是下面的代码有什么问题吗 var mtlLoader new THREE MTLLoader mtlLoader setPath objects Tree mt
  • Fabric JS ClipPath:裁剪后如何使图像适合画布?

    我使用 FabricJS 和 ClipPath 属性实现了图像裁剪 问题是如何使裁剪后的图像适合画布 我希望裁剪后的图像填充画布区域 但不知道是否可以使用 Fabric js 来完成 因此 我希望用户单击 裁剪 按钮后图像的选定部分适合画布
  • 将数据附加到 #div 时如何防止 javascript 中的重复输入

    PFB JavaScript 代码 问题是我收到重复条目的警报 如何避免重复数据 Var activityconunt 0 if activityconunt data iRoundId alert duplicate selectRoun
  • useReducer 未同步更新状态的问题

    根据React docs 当你有复杂的情况时 useReducer 通常比 useState 更好 涉及多个子值或下一个状态时的状态逻辑 取决于前一个 1 有人可以解释一下为什么吗useReducer不是同步更新状态吗 const redu
  • 如何使用 Node JS 对包含小数/尾随零的数据生成哈希

    在尝试验证 Node JS 中的 Authorize net webhook 通知时 我遇到了以下与小数 尾随零有关的问题 Authorize net 使用 HMAC SHA512 以及 Webhook 通知正文和商家的签名密钥形成哈希 该
  • Javascript 函数无法返回元素

    所以我正在与手动表 http handsontable com 目前项目的 jQuery 插件 我已经编写了一些自定义函数来使用它 我目前遇到问题的函数是我编写的一个函数 用于返回当前选定的单元格 当用户仅选择一个而不是多个时 是的 已检查
  • NEXT JS - 应用程序通过重写呈现两次

    我正在开发一个 NEXT JS 项目 但找不到两次渲染应用程序的解决方案 Problem 添加后rewrites to next config js App被渲染两次 如果移除 则渲染一次 next config js async rewr
  • Node.js 无限循环功能,在某些用户输入时退出

    我不太明白节点在异步和循环方面到底是如何工作的 我想在这里实现的是让控制台打印出来 Command 并等待用户的输入 但在等待时我希望它运行 一些随机函数 无休止地直到用户在终端上输入 exit 感谢所有的帮助 以及可能的解释 以便我能理解
  • javascript检测字符串是否只包含unicode表情符号[重复]

    这个问题在这里已经有答案了 我正在使用以下函数来替换字符串中的表情符号 并且效果很好 function doEmoji s var ranges ud83c udf00 udfff U 1F300 to U 1F3FF ud83d udc0
  • 如何在firebase云函数中添加时间戳

    我正在尝试添加Timestamp在有关 Firebase Cloud 功能的 Firestore 文档中 我曾尝试过firestore Timestamp fromDate new Date 但它不起作用 const functions r
  • 每 x 秒对 SVG 元素进行动画处理

    介绍 我了解一些基本的动画技术SVG两者同时使用Javascript和 DOM

随机推荐

  • 【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核心概念 状态提升 引言 添加第二个输入框 编写转换函数 状态提升 经验总结 引言 很多情况下我们使用的多个组件需要对同一个数据做出对应的反应 在这里我们推荐把这个共享的状态提升到距离这些组件最