React 18 对 state 进行保留和重置

参考文章

对 state 进行保留和重置

各个组件的 state 是各自独立的。根据组件在 UI 树中的位置,React 可以跟踪哪些 state 属于哪个组件。可以控制在重新渲染过程中何时对 state 进行保留和重置。

UI 树

浏览器使用许多树形结构来为 UI 建立模型。DOM 用于表示 HTML 元素,CSSOM 则表示 CSS 元素。甚至还有 Accessibility tree!

React 也使用树形结构来对创造的 UI 进行管理和建模。React 根据 JSX 生成 UI 树。React DOM 根据 UI 树去更新浏览器的 DOM 元素。(React Native 则将这些 UI 树转译成移动平台上特有的元素。)

在这里插入图片描述

state 与树中的某个位置相关联

当为一个组件添加 state 时,可能会觉得 state “活”在组件内部。但实际上,state 被保存在 React 内部根据组件在 UI 树中的位置,React 将它所持有的每个 state 与正确的组件关联起来

下面只定义了一个 <Counter /> JSX 标签,但将它渲染在了两个不同的位置:

import { useState } from 'react';export default function App() {const counter = <Counter />;return (<div>{counter}{counter}</div>);
}function Counter() {const [score, setScore] = useState(0);const [hover, setHover] = useState(false);let className = 'counter';if (hover) {className += ' hover';}return (<divclassName={className}onPointerEnter={() => setHover(true)}onPointerLeave={() => setHover(false)}><h1>{score}</h1><button onClick={() => setScore(score + 1)}>加一</button></div>);
}

下面是它们的树形结构的样子:

在这里插入图片描述

这是两个独立的 counter,因为它们在树中被渲染在了各自的位置。 一般情况下不用去考虑这些位置来使用 React,但知道它们是如何工作会很有用。

在 React 中,屏幕中的每个组件都有完全独立的 state。举个例子,当并排渲染两个 Counter 组件时,它们都会拥有各自独立的 scorehover state。

试试点击两个 counter 会发现它们互不影响:

import { useState } from 'react';export default function App() {return (<div><Counter /><Counter /></div>);
}...

如你所见,当一个计数器被更新时,只有该组件的状态会被更新:

在这里插入图片描述

只有当在相同的位置渲染相同的组件时,React 才会一直保留着组件的 state。想要验证这一点,可以将两个计数器的值递增,取消勾选 “渲染第二个计数器” 复选框,然后再次勾选它:

import { useState } from 'react';export default function App() {const [showB, setShowB] = useState(true);return (<div><Counter />{showB && <Counter />} <label><inputtype="checkbox"checked={showB}onChange={e => {setShowB(e.target.checked)}}/>渲染第二个计数器</label></div>);
}...

注意:当停止渲染第二个计数器的那一刻,它的 state 完全消失了。这是因为 React 在移除一个组件时,也会销毁它的 state

在这里插入图片描述

当重新勾选“渲染第二个计数器”复选框时,另一个计数器及其 state 将从头开始初始化(score = 0)并被添加到 DOM 中。

在这里插入图片描述

只要一个组件还被渲染在 UI 树的相同位置,React 就会保留它的 state。 如果它被移除,或者一个不同的组件被渲染在相同的位置,那么 React 就会丢掉它的 state。

相同位置的相同组件会使得 state 被保留下来

在这个例子中,有两个不同的 Counter 标签:

import { useState } from 'react';export default function App() {const [isFancy, setIsFancy] = useState(false);return (<div>{isFancy ? (<Counter isFancy={true} /> ) : (<Counter isFancy={false} /> )}<label><inputtype="checkbox"checked={isFancy}onChange={e => {setIsFancy(e.target.checked)}}/>使用好看的样式</label></div>);
}function Counter({ isFancy }) {const [score, setScore] = useState(0);const [hover, setHover] = useState(false);let className = 'counter';if (hover) {className += ' hover';}if (isFancy) {className += ' fancy';}return (<divclassName={className}onPointerEnter={() => setHover(true)}onPointerLeave={() => setHover(false)}><h1>{score}</h1><button onClick={() => setScore(score + 1)}>加一</button></div>);
}

当勾选或清空复选框的时候,计数器 state 并没有被重置。不管 isFancytrue 还是 false,根组件 App 返回的 div 的第一个子组件都是 <Counter />

在这里插入图片描述

它是位于相同位置的相同组件,所以对 React 来说,它是同一个计数器。

注意: 对 React 来说,重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置! 这个组件在 if 内外有两个return 语句,它们带有不同的 <Counter /> JSX 标签:

import { useState } from 'react';export default function App() {const [isFancy, setIsFancy] = useState(false);if (isFancy) {return (<div><Counter isFancy={true} /><label><inputtype="checkbox"checked={isFancy}onChange={e => {setIsFancy(e.target.checked)}}/>使用好看的样式</label></div>);}return (<div><Counter isFancy={false} /><label><inputtype="checkbox"checked={isFancy}onChange={e => {setIsFancy(e.target.checked)}}/>使用好看的样式</label></div>);
}...

可能以为当勾选复选框的时候 state 会被重置,但它并没有!这是因为 两个 <Counter /> 标签被渲染在了相同的位置。 React 不知道函数里是如何进行条件判断的,它只会“看到”返回的树。在这两种情况下,App 组件都会返回一个包裹着 <Counter /> 作为第一个子组件的 div。这就是 React 认为它们是 同一个 <Counter /> 的原因。

可以认为它们有相同的“地址”:根组件的第一个子组件的第一个子组件。不管你的逻辑是怎么组织的,这就是 React 在前后两次渲染之间将它们进行匹配的方式。

相同位置的不同组件会使 state 重置

在这个例子中,勾选复选框会将 <Counter> 替换为一个 <p>

import { useState } from 'react';export default function App() {const [isPaused, setIsPaused] = useState(false);return (<div>{isPaused ? (<p>待会见!</p> ) : (<Counter /> )}<label><inputtype="checkbox"checked={isPaused}onChange={e => {setIsPaused(e.target.checked)}}/>休息一下</label></div>);
}...
}

示例中,在相同位置对 不同 的组件类型进行切换。刚开始 <div> 的第一个子组件是一个 Counter。但是当切换成 p 时,React 将 Counter 从 UI 树中移除了并销毁了它的状态。

在这里插入图片描述

并且,当在相同位置渲染不同的组件时,组件的整个子树都会被重置。要验证这一点,可以增加计数器的值然后勾选复选框:

import { useState } from 'react';export default function App() {const [isFancy, setIsFancy] = useState(false);return (<div>{isFancy ? (<div><Counter isFancy={true} /> </div>) : (<section><Counter isFancy={false} /></section>)}<label><inputtype="checkbox"checked={isFancy}onChange={e => {setIsFancy(e.target.checked)}}/>使用好看的样式</label></div>);
}...

当勾选复选框后计数器的 state 被重置了。虽然渲染了一个 Counter,但是 div 的第一个子组件从 div 变成了 section。当子组件 div 从 DOM 中被移除的时候,它底下的整棵树(包含 Counter 以及它的 state)也都被销毁了。

在这里插入图片描述

一般来说,如果想在重新渲染时保留 state,几次渲染中的树形结构就应该相互“匹配”。结构不同就会导致 state 的销毁,因为 React 会在将一个组件从树中移除时销毁它的 state

不应该把组件函数的定义嵌套起来

以下是为什么不应该把组件函数的定义嵌套起来的原因。

示例中, MyTextField 组件被定义在 MyComponent 内部

import { useState } from 'react';export default function MyComponent() {const [counter, setCounter] = useState(0);function MyTextField() {const [text, setText] = useState('');return (<inputvalue={text}onChange={e => setText(e.target.value)}/>);}return (<><MyTextField /><button onClick={() => {setCounter(counter + 1)}}>点击了 {counter}</button></>);
}

每次点击按钮,输入框的 state 都会消失!这是因为每次 MyComponent 渲染时都会创建一个 不同MyTextField 函数。在相同位置渲染的是 不同 的组件,所以 React 将其下所有的 state 都重置了。这样会导致 bug 以及性能问题。为了避免这个问题, 永远要将组件定义在最上层并且不要把它们的定义嵌套起来。

在相同位置重置 state

默认情况下,React 会在一个组件保持在同一位置时保留它的 state。通常这就是你想要的,所以把它作为默认特性很合理。但有时候,可能想要重置一个组件的 state。考虑一下这个应用,它可以让两个玩家在每个回合中记录他们的得分:

import { useState } from 'react';export default function Scoreboard() {const [isPlayerA, setIsPlayerA] = useState(true);return (<div>{isPlayerA ? (<Counter person="Taylor" />) : (<Counter person="Sarah" />)}<button onClick={() => {setIsPlayerA(!isPlayerA);}}>下一位玩家!</button></div>);
}function Counter({ person }) {const [score, setScore] = useState(0);const [hover, setHover] = useState(false);let className = 'counter';if (hover) {className += ' hover';}return (<divclassName={className}onPointerEnter={() => setHover(true)}onPointerLeave={() => setHover(false)}><h1>{person} 的分数:{score}</h1><button onClick={() => setScore(score + 1)}>加一</button></div>);
}

目前当切换玩家时,分数会被保留下来。这两个 Counter 出现在相同的位置,所以 React 会认为它们是 同一个 Counter,只是传了不同的 person prop。

但是从概念上讲,这个应用中的两个计数器应该是各自独立的。虽然它们在 UI 中的位置相同,但是一个是 Taylor 的计数器,一个是 Sarah 的计数器。

有两个方法可以在它们相互切换时重置 state:

  1. 将组件渲染在不同的位置
  2. 使用 key 赋予每个组件一个明确的身份

方法一:将组件渲染在不同的位置

如果想让两个 Counter 各自独立的话,可以将它们渲染在不同的位置:

import { useState } from 'react';export default function Scoreboard() {const [isPlayerA, setIsPlayerA] = useState(true);return (<div>{isPlayerA &&<Counter person="Taylor" />}{!isPlayerA &&<Counter person="Sarah" />}<button onClick={() => {setIsPlayerA(!isPlayerA);}}>下一位玩家!</button></div>);
}...
  • 起初 isPlayerA 的值是 true。所以第一个位置包含了 Counter 的 state,而第二个位置是空的。
  • 当你点击“下一位玩家”按钮时,第一个位置会被清空,而第二个位置现在包含了一个 Counter

在这里插入图片描述

每次一个 Counter 被从 DOM 中移除时,它的 state 就会被销毁。这就是每次点击按钮时它们就会被重置的原因。

这个解决方案在只有少数几个独立的组件渲染在相同的位置时会很方便。这个例子中只有 2 个组件,所以在 JSX 里将它们分开进行渲染并不麻烦。

方法二:使用 key 来重置 state

还有另一种更通用的重置组件 state 的方法。

可能在 渲染列表 时见到过 key。但 key 不只可以用于列表!可以使用 key 来让 React 区分任何组件。默认情况下,React 使用父组件内部的顺序(“第一个计数器”、“第二个计数器”)来区分组件。但是 key 可以让你告诉 React 这不仅仅是 第一个 或者 第二个 计数器,而且还是一个特定的计数器——例如,Taylor 的 计数器。这样无论它出现在树的任何位置, React 都会知道它是 Taylor 的 计数器!

在这个例子中,即使两个 <Counter /> 会出现在 JSX 中的同一个位置,它们也不会共享 state:

import { useState } from 'react';export default function Scoreboard() {const [isPlayerA, setIsPlayerA] = useState(true);return (<div>{isPlayerA ? (<Counter key="Taylor" person="Taylor" />) : (<Counter key="Sarah" person="Sarah" />)}<button onClick={() => {setIsPlayerA(!isPlayerA);}}>下一位玩家!</button></div>);
}...

在 Taylor 和 Sarah 之间切换不会使 state 被保留下来。因为 给他们赋了不同的 key

{isPlayerA ? (<Counter key="Taylor" person="Taylor" />) : (<Counter key="Sarah" person="Sarah" />
)}

指定一个 key 能够让 React 将 key 本身而非它们在父组件中的顺序作为位置的一部分。这就是为什么尽管用 JSX 将组件渲染在相同位置,但在 React 看来它们是两个不同的计数器。因此它们永远都不会共享 state。每当一个计数器出现在屏幕上时,它的 state 会被创建出来。每当它被移除时,它的 state 就会被销毁。在它们之间切换会一次又一次地使它们的 state 重置。

注意:请记住 key 不是全局唯一的。它们只能指定 父组件内部 的顺序。

使用 key 重置表单

使用 key 来重置 state 在处理表单时特别有用。

在这个聊天应用中, <Chat> 组件包含文本输入 state:

// App.js
import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';export default function Messenger() {const [to, setTo] = useState(contacts[0]);return (<div><ContactListcontacts={contacts}selectedContact={to}onSelect={contact => setTo(contact)}/><Chat contact={to} /></div>)
}const contacts = [{ id: 0, name: 'Taylor', email: 'taylor@mail.com' },{ id: 1, name: 'Alice', email: 'alice@mail.com' },{ id: 2, name: 'Bob', email: 'bob@mail.com' }
];
// ContactList.js
export default function ContactList({selectedContact,contacts,onSelect
}) {return (<section className="contact-list"><ul>{contacts.map(contact =><li key={contact.id}><button onClick={() => {onSelect(contact);}}>{contact.name}</button></li>)}</ul></section>);
}
// Chat.js
import { useState } from 'react';export default function Chat({ contact }) {const [text, setText] = useState('');return (<section className="chat"><textareavalue={text}placeholder={'跟 ' + contact.name + ' 聊一聊'}onChange={e => setText(e.target.value)}/><br /><button>发送到 {contact.email}</button></section>);
}

尝试在输入框中输入一些内容,然后点击 “Alice” 或 “Bob” 来选择不同的收件人。会发现因为 <Chat> 被渲染在了树的相同位置,输入框的 state 被保留下来了。

在很多应用里这可能会是大家所需要的特性,但在这个聊天应用里并不是! 不应该让用户因为一次偶然的点击而把他们已经输入的信息发送给一个错误的人。要修复这个问题,只需给组件添加一个 key

<Chat key={to.id} contact={to} />

这样确保了当选择一个不同的收件人时, Chat 组件——包括其下方树中的任何 state——都将从头开始重新创建。 React 还将重新创建 DOM 元素,而不是复用它们。

现在切换收件人就总会清除文本字段了:

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';export default function Messenger() {const [to, setTo] = useState(contacts[0]);return (<div><ContactListcontacts={contacts}selectedContact={to}onSelect={contact => setTo(contact)}/><Chat key={to.id} contact={to} /></div>)
}const contacts = [{ id: 0, name: 'Taylor', email: 'taylor@mail.com' },{ id: 1, name: 'Alice', email: 'alice@mail.com' },{ id: 2, name: 'Bob', email: 'bob@mail.com' }
];

摘要

  • 只要在相同位置渲染的是相同组件, React 就会保留状态。
  • state 不会被保存在 JSX 标签里。它与在树中放置该 JSX 的位置相关联。
  • 可以通过为一个子树指定一个不同的 key 来重置它的 state。
  • 不要嵌套组件的定义,否则会意外地导致 state 被重置。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/114836.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

25 Linux可视化-Webmin和bt运维工具

25 Linux可视化-Webmin和bt运维工具 文章目录 25 Linux可视化-Webmin和bt运维工具25.1 Web运行环境简介25.2 Webmin的安装及使用25.2.1 安装webmin25.2.2 Webmin使用演示 25.3 bt(宝塔)的安装及使用25.3.1 安装宝塔25.3.2 宝塔Web登录Linux服务器25.3.3 找回宝塔登录密码 学习视…

re学习(36)看雪CTF 2017 -Crackme

百度网盘已收集&#xff0c;需要回顾在文件夹【CTF题库收集】查看即可 刚开始先运行一下 这是一道crackme类型题目&#xff0c;是一道看雪CTF上的竞赛题&#xff0c;用OD进行分析一下。 刚开始用的是IDA分析&#xff0c;分析了半天也没有看出来什么&#xff0c;然后就在网上搜…

汽车服务门店小程序模板制作指南

在数字化时代&#xff0c;一个小程序的力量不可忽视。它不仅是展示品牌形象和提供用户服务的重要工具&#xff0c;更是扩大客户群体和提高营收的关键手段。对于汽车服务门店来说&#xff0c;拥有一个精美且功能齐全的小程序&#xff0c;更将成为你在竞争激烈的市场中的重要武器…

终端登录github两种方式

第一种方式 添加token&#xff0c;Setting->Developer Setting 第二种方式SSH 用下面命令查看远程仓库格式 git remote -v 用下面命令更改远程仓库格式 git remote set-url origin gitgithub.com:用户名/仓库名.git 然后用下面命令生成新的SSH秘钥 ssh-keygen -t ed2…

1.Python操作txt文本

文章目录 1.Python读取一个txt文件的内容并将其写入到另一个txt文件2.Python读取一个未知编码的文件并将其设置为指定编码格式3.Python实现txt文件中字符串的替换 1.Python读取一个txt文件的内容并将其写入到另一个txt文件 # -*- encoding:gb2312 -*- import chardetdef read_…

Qt应用开发(基础篇)——日历 QCalendarWidget

一、前言 QCalendarWidget类继承于QWidget&#xff0c;是Qt设计用来让用户更直观的选择日期的窗口部件。 时间微调输入框 QCalendarWidget根据年份和月份初始化&#xff0c;程序员也通过提供公共函数去改变他们&#xff0c;默认日期为当前的系统时间&#xff0c;用户通过鼠标和…

indexDb使用

indexDb是什么&#xff1f; indexDb是除了cookie&#xff0c;localstorage&#xff0c;sessionstroage外的另一种前端存贮方式。 现有前端存贮比较 indexDb特点 无大小限制&#xff0c;适用于前端存贮数据较多场景存贮结构以对象仓库形式&#xff0c;可以存入任何类型数据&a…

无重叠区间【贪心算法】

无重叠区间 给定一个区间的集合 intervals &#xff0c;其中 intervals[i] [starti, endi] 。返回 需要移除区间的最小数量&#xff0c;使剩余区间互不重叠 。 class Solution {public int eraseOverlapIntervals(int[][] intervals) {//先排序&#xff0c;按照左边界升序,注…

MySQL 数据库常用命令大全(详细)

文章目录 1. MySQL命令2. MySQL基础命令3. MySQL命令简介4. MySQL常用命令4.1 MySQL准备篇4.1.1 启动和停止MySQL服务4.1.2 修改MySQL账户密码4.1.3 MySQL的登陆和退出4.1.4 查看MySQL版本 4.2 DDL篇&#xff08;数据定义&#xff09;4.2.1 查询数据库4.2.2 创建数据库4.2.3 使…

接口自动化测试:mock server之Moco工具

什么是mock server mock&#xff1a;英文可以翻译为模仿的&#xff0c;mock server是我们用来解除依赖&#xff08;耦合&#xff09;&#xff0c;假装实现的技术&#xff0c;比如说&#xff0c;前端需要使用某些api进行调试&#xff0c;但是服务端并没有开发完成这些api&#…

iOS import包

Frameworks Frameworks 顾名思义就是框架&#xff0c;是第三方打包完成看不到源码&#xff0c;可以直接使用的 在项目中引用方式 OC 引用某一个文件&#xff0c;Frameworks一般会提供一个h文件引用全部其他文件 #import <JLRoutes/JLRoutes.h>swift 引用一个包&#x…

vue使用qrcodejs2生成二维码

目录 概要 构建展示的vue组件qrcode.vue 组件的使用 概要 项目中用到需要展示二维码的样式&#xff0c;想到了qrcode 例如&#xff1a; 前提&#xff1a;安装包 npm install qrcodejs2 --save 构建展示的vue组件qrcode.vue <template><div style"width: …

JVM垃圾回收算法和CMS垃圾收集器

目录 判断一个对象是否死亡&#xff1f; 1、引用计数法 2、可达性分析算法 三色标记 垃圾收集算法 1、分代收集理论 2、垃圾回收算法 标记-清除 标记-复制 标记-整理 CMS&#xff08;Concurrent Mark Sweep&#xff09;收集器 CMS垃圾收集器步骤 CMS垃圾收集器优…

气传导耳机排名,四款市面上热销相当不错的气传导耳机推荐

​在当今快速的生活节奏中&#xff0c;气传导耳机成为了越来越多人的选择。它们以出色的音质和舒适度而广受好评。在本文中&#xff0c;我们将为您推荐四款市面上热销相当不错的气传导耳机&#xff0c;帮助你找到最适合自己的耳机。 推荐一&#xff1a;NANK南卡00压开放式耳机…

视觉化洞察:为什么我们需要数据可视化?

为什么我们需要数据可视化&#xff1f;这个问题在信息时代变得愈发重要。数据&#xff0c;如今已成为生活的一部分&#xff0c;我们每天都在产生大量的数据&#xff0c;从社交媒体到购物记录&#xff0c;从健康数据到工作表现&#xff0c;数据无处不在。然而&#xff0c;数据本…

基于RabbitMQ的模拟消息队列需求文档

文章目录 一、项目背景二、需求分析1.核心概念2.BrokerServer核心组件3.核心API4.交换机类型5.持久化6.网络通信7.消息应答 三、消息队列模块划分 一、项目背景 什么是消息队列&#xff1f; 消息队列就是&#xff0c;基于阻塞队列&#xff0c;封装成一个独立的服务器程序&#…

【Ubuntu】Ubuntu常用软件部署

1.安装jdk1.8 (1).apt方式安装 1).安装 1.在终端中输入以下命令&#xff0c;以更新软件包列表 sudo apt-get update2.在终端中输入以下命令&#xff0c;以安装JDK 1.8 sudo apt-get install openjdk-8-jdk3.将Java 1.8设置为默认版本。在终端中输入以下命令 sudo update-…

【Linux系列】使用虚拟机安装Linux系统

首发博客地址 首发博客地址[1] 系列文章地址[2] 下载虚拟机 请从官网下载&#xff1a; https://customerconnect.vmware.com/en/downloads/info/slug/desktop_end_user_computing/vmware_workstation_player/17_0 如需不限速下载&#xff0c;请关注【程序员朱永胜】并回复 1018…

微芯I/O控制器瞄准工业与嵌入式运算应用

微芯科技(Microchip Technology)日前发布SCH322X系列I/O控制器新品&#xff0c;该系列产品基于工业及嵌入式开发工程师的需求而开发&#xff0c;功能丰富且具高灵活性。新一代I/O控制器系列拥有尺寸更小的包装和更长的产品生命周期&#xff0c;可运用于更多经济型工业及嵌入式开…

Android安卓实战项目(12)—关于身体分析,BMI计算,喝水提醒,食物卡路里计算APP【支持中英文切换】生活助手类APP(源码在文末)

Android安卓实战项目&#xff08;12&#xff09;—关于身体分析&#xff0c;BMI计算&#xff0c;喝水提醒&#xff0c;食物卡路里计算APP【支持中英文切换】生活助手类APP&#xff08;源码在文末&#x1f415;&#x1f415;&#x1f415;&#xff09; 一.项目运行介绍 B站演示…