React-状态管理
React 控制 UI 的方式是声明式的。你不必直接控制 UI 的各个部分,只需要声明组件可以处于的不同状态,…
用 State 响应输入
React 控制 UI 的方式是声明式的。你不必直接控制 UI 的各个部分,只需要声明组件可以处于的不同状态,并根据用户的输入在它们之间切换。这与设计师对 UI 的思考方式很相似。
声明式 UI 与命令式 UI 的比较
当你设计 UI 交互时,可能会去思考 UI 如何根据用户的操作而响应变化。想象一个让用户提交答案的表单:
- 当你向表单输入数据时,“提交”按钮会随之变成可用状态
- 当你点击“提交”后,表单和提交按钮都会随之变成不可用状态,并且会加载动画会随之出现
- 如果网络请求成功,表单会随之隐藏,同时“提交成功”的信息会随之出现
- 如果网络请求失败,错误信息会随之出现,同时表单又变为可用状态
在 命令式编程 中,以上的过程直接告诉你如何去实现交互。你必须去根据要发生的事情写一些明确的命令去操作 UI。对此有另一种理解方式。
React 正是为了解决这样的问题而诞生的。
在 React 中,你不必直接去操作 UI —— 你不必直接启用、关闭、显示或隐藏组件。相反,你只需要 声明你想要显示的内容, React 就会通过计算得出该如何去更新 UI。
声明式地考虑 UI
接下来你将会学到如何用 React 重新实现这个 UI:
- 定位你的组件中不同的视图状态
- 确定是什么触发了这些 state 的改变
- 表示内存中的 state(需要使用
useState) - 删除任何不必要的 state 变量
- 连接事件处理函数去设置 state
步骤 1:定位组件中不同的视图状态
首先,你需要去可视化 UI 界面中用户可能看到的所有不同的“状态”:
- 无数据:表单有一个不可用状态的“提交”按钮。
- 输入中:表单有一个可用状态的“提交”按钮。
- 提交中:表单完全处于不可用状态,加载动画出现。
- 成功时:显示“成功”的消息而非表单。
- 错误时:与输入状态类似,但会多错误的消息。
像一个设计师一样,你会想要在你添加逻辑之前去“模拟”不同的状态或创建“模拟状态”。例如下面的例子,这是一个对表单可视部分的模拟。这个模拟被一个 status 的属性控制,并且这个属性的默认值为 empty。
export default function Form({
status = 'empty'
}) {
if (status === 'success') {
return <h1>That's right!</h1>
}
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form>
<textarea />
<br />
<button>
Submit
</button>
</form>
</>
)
}
你可以随意命名这个属性,名字并不重要。试着将 status = 'empty' 改为 status = 'success',然后你就会看到成功的信息出现。模拟可以让你在书写逻辑前快速迭代 UI。这是同一组件的一个更加充实的原型,仍然由 status 属性“控制”:
export default function Form({
// Try 'submitting', 'error', 'success':
status = 'empty'
}) {
if (status === 'success') {
return <h1>That's right!</h1>
}
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form>
<textarea disabled={
status === 'submitting'
} />
<br />
<button disabled={
status === 'empty' ||
status === 'submitting'
}>
Submit
</button>
{status === 'error' &&
<p className="Error">
Good guess but a wrong answer. Try again!
</p>
}
</form>
</>
);
}
步骤 2:确定是什么触发了这些状态的改变
你可以触发 state 的更新来响应两种输入:
- 人为输入。比如点击按钮、在表单中输入内容,或导航到链接。
- 计算机输入。比如网络请求得到反馈、定时器被触发,或加载一张图片。

以上两种情况中,你必须设置 state 变量 去更新 UI。对于正在开发中的表单来说,你需要改变 state 以响应几个不同的输入:
- 改变输入框中的文本时(人为)应该根据输入框的内容是否是空值,从而决定将表单的状态从空值状态切换到输入中或切换回原状态。
- 点击提交按钮时(人为)应该将表单的状态切换到提交中的状态。
- 网络请求成功后(计算机)应该将表单的状态切换到成功的状态。
- 网络请求失败后(计算机)应该将表单的状态切换到失败的状态,与此同时,显示错误信息。
注意,人为输入通常需要 事件处理函数!
为了可视化这个流程,请尝试在纸上画出圆形标签以表示每个状态,两个状态之间的改变用箭头表示。你可以像这样画出很多流程并且在写代码前解决许多 bug。

步骤 3:通过 useState 表示内存中的 state
接下来你会需要在内存中通过 useState 表示组件中的视图状态。
先从绝对必须存在的状态开始。例如,你需要存储输入的 answer 以及用于存储最后一个错误的 error (如果存在的话):
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);接下来,你需要一个状态变量来代表你想要显示的那个可视状态。通常有多种方式在内存中表示它,因此你需要进行实验。
如果你很难立即想出最好的办法,那就先从添加足够多的 state 开始,确保所有可能的视图状态都囊括其中:
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);你最初的想法或许不是最好的,但是没关系,重构 state 也是步骤中的一部分!
步骤 4:删除任何不必要的 state 变量
你会想要避免 state 内容中的重复,从而只需要关注那些必要的部分。花一点时间来重构你的 state 结构,会让你的组件更容易被理解,减少重复并且避免歧义。你的目的是防止出现在内存中的 state 不代表任何你希望用户看到的有效 UI 的情况。(比如你绝对不会想要在展示错误信息的同时禁用掉输入框,导致用户无法纠正错误!)
这有一些你可以问自己的, 关于 state 变量的问题:
- 这个 state 是否会导致矛盾?例如,
isTyping与isSubmitting的状态不能同时为true。矛盾的产生通常说明了这个 state 没有足够的约束条件。两个布尔值有四种可能的组合,但是只有三种对应有效的状态。为了将“不可能”的状态移除,你可以将他们合并到一个'status'中,它的值必须是'typing'、'submitting'以及'success'这三个中的一个。 - 相同的信息是否已经在另一个 state 变量中存在?另一个矛盾:
isEmpty和isTyping不能同时为true。通过使它们成为独立的 state 变量,可能会导致它们不同步并导致 bug。幸运的是,你可以移除isEmpty转而用message.length === 0。 - 你是否可以通过另一个 state 变量的相反值得到相同的信息?
isError是多余的,因为你可以检查error !== null。
在清理之后,你只剩下 3 个(从原本的 7 个!)必要的 state 变量:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'正是因为你不能在不破坏功能的情况下删除其中任何一个状态变量,因此你可以确定这些都是必要的。
通过 reducer 来减少“不可能” state
尽管这三个变量对于表示这个表单的状态来说已经足够好了,仍然是有一些中间状态并不是完全有意义的。例如一个非空的
error当status的值为success时没有意义。为了更精确地模块化状态,你可以 将状态提取到一个 reducer 中。Reducer 可以让你合并多个状态变量到一个对象中并巩固所有相关的逻辑!
步骤 5:连接事件处理函数以设置 state
最后,创建事件处理函数去设置 state 变量。下面是绑定好事件的最终表单:
import { useState } from 'react';
export default function Form() {
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing');
if (status === 'success') {
return <h1>That's right!</h1>
}
async function handleSubmit(e) {
e.preventDefault();
setStatus('submitting');
try {
await submitForm(answer);
setStatus('success');
} catch (err) {
setStatus('typing');
setError(err);
}
}
function handleTextareaChange(e) {
setAnswer(e.target.value);
}
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form onSubmit={handleSubmit}>
<textarea
value={answer}
onChange={handleTextareaChange}
disabled={status === 'submitting'}
/>
<br />
<button disabled={
answer.length === 0 ||
status === 'submitting'
}>
Submit
</button>
{error !== null &&
<p className="Error">
{error.message}
</p>
}
</form>
</>
);
}
function submitForm(answer) {
// Pretend it's hitting the network.
return new Promise((resolve, reject) => {
setTimeout(() => {
let shouldError = answer.toLowerCase() !== 'lima'
if (shouldError) {
reject(new Error('Good guess but a wrong answer. Try again!'));
} else {
resolve();
}
}, 1500);
});
}
尽管这些代码相对与最初的命令式的例子来说更长,但是却更加健壮。将所有的交互变为 state 的改变,可以让你避免之后引入新的视图状态后导致现有 state 被破坏。同时也使你在不必改变交互逻辑的情况下,更改每个状态对应的 UI。
选择 State 结构
构建良好的 state 可以让组件变得易于修改和调试,而不会经常出错。以下是你在构建 state 时应该考虑的一些建议。
构建 state 的原则
当你编写一个存有 state 的组件时,你需要选择使用多少个 state 变量以及它们都是怎样的数据格式。尽管选择次优的 state 结构下也可以编写正确的程序,但有几个原则可以指导你做出更好的决策:
- 合并关联的 state。如果你总是同时更新两个或更多的 state 变量,请考虑将它们合并为一个单独的 state 变量。
- 避免互相矛盾的 state。当 state 结构中存在多个相互矛盾或“不一致”的 state 时,你就可能为此会留下隐患。应尽量避免这种情况。
- 避免冗余的 state。如果你能在渲染期间从组件的 props 或其现有的 state 变量中计算出一些信息,则不应将这些信息放入该组件的 state 中。
- 避免重复的 state。当同一数据在多个 state 变量之间或在多个嵌套对象中重复时,这会很难保持它们同步。应尽可能减少重复。
- 避免深度嵌套的 state。深度分层的 state 更新起来不是很方便。如果可能的话,最好以扁平化方式构建 state。
这些原则背后的目标是 使 state 易于更新而不引入错误。从 state 中删除冗余和重复数据有助于确保所有部分保持同步。这类似于数据库工程师想要 “规范化”数据库结构,以减少出现错误的机会。用爱因斯坦的话说,**“让你的状态尽可能简单,但不要过于简单。”
合并关联的 state
有时候你可能会不确定是使用单个 state 变量还是多个 state 变量。
你会像下面这样做吗?
const [x, setX] = useState(0);
const [y, setY] = useState(0);或这样?
const [position, setPosition] = useState({ x: 0, y: 0 });从技术上讲,你可以使用其中任何一种方法。但是,如果某两个 state 变量总是一起变化,则将它们统一成一个 state 变量可能更好。
另一种你需要将数据整合到一个对象或一个数组的情况是,你不知道未来需要多少个 state 片段。例如,有一个用户可以添加自定义字段的表单时,这将会很有帮助。
避免矛盾的 state
下面是带有 isSending 和 isSent 两个 state 变量的酒店反馈表单:
import { useState } from 'react';
export default function FeedbackForm() {
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setIsSending(true);
await sendMessage(text);
setIsSending(false);
setIsSent(true);
}
return (
...
);
}尽管这段代码是有效的,但也会让一些 state “极难处理”。例如,如果你忘记同时调用 setIsSent 和 setIsSending,则可能会出现 isSending 和 isSent 同时为 true 的情况。你的组件越复杂,你就越难理解发生了什么。
因为 isSending 和 isSent 不应同时为 true,所以最好用一个 status 变量来代替它们,这个 state 变量可以采取三种有效状态其中之一:'typing' (初始), 'sending', 和 'sent':
import { useState } from 'react';
export default function FeedbackForm() {
const [text, setText] = useState('');
const [status, setStatus] = useState('typing');
async function handleSubmit(e) {
e.preventDefault();
setStatus('sending');
await sendMessage(text);
setStatus('sent');
}
const isSending = status === 'sending';
const isSent = status === 'sent';
...
}你仍然可以声明一些常量,以提高可读性:
const isSending = status === 'sending';
const isSent = status === 'sent';但它们不是 state 变量,所以你不必担心它们彼此失去同步。
避免冗余的 state
如果你能在渲染期间从组件的 props 或其现有的 state 变量中计算出一些信息,则不应该把这些信息放到该组件的 state 中。
例如,以这个表单为例。它可以运行,但你能找到其中任何冗余的 state 吗?
import { useState } from 'react';
export default function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
function handleFirstNameChange(e) {
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
setFullName(firstName + ' ' + e.target.value);
}
return (
...
);
}
这个表单有三个 state 变量:firstName、lastName 和 fullName。然而,fullName 是多余的。在渲染期间,你始终可以从 firstName 和 lastName 中计算出 fullName,因此需要把它从 state 中删除。
你可以这样做:
import { useState } from 'react';
export default function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = firstName + ' ' + lastName;
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
return (
...
);
}
这里的 fullName 不是 一个 state 变量。相反,它是在渲染期间中计算出的:
const fullName = firstName + ' ' + lastName;因此,更改处理程序不需要做任何特殊操作来更新它。当你调用 setFirstName 或 setLastName 时,你会触发一次重新渲染,然后下一个 fullName 将从新数据中计算出来。
备注
不要在 state 中镜像 props
以下代码是体现 state 冗余的一个常见例子:
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);这里,一个 color state 变量被初始化为 messageColor 的 prop 值。这段代码的问题在于,如果父组件稍后传递不同的 messageColor 值(例如,将其从 'blue' 更改为 'red'),则 color state 变量将不会更新! state 仅在第一次渲染期间初始化。
按照惯例,prop 名称以 initial 或 default 开头,以阐明该 prop 的新值将被忽略:
function Message({ initialColor }) {
// 这个 `color` state 变量用于保存 `initialColor` 的 **初始值**。
// 对于 `initialColor` 属性的进一步更改将被忽略。
const [color, setColor] = useState(initialColor);避免重复的 state
下面这个菜单列表组件可以让你在多种旅行小吃中选择一个:
import { useState } from 'react';
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
export default function Menu() {
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(
items[0]
);
return (
<>
...
</>
);
}
当前,它将所选元素作为对象存储在 selectedItem state 变量中。然而,这并不好:selectedItem 的内容与 items 列表中的某个项是同一个对象。 这意味着关于该项本身的信息在两个地方产生了重复。
提示
但是在更新items数组的时候,由于react希望我们每次传的都是全新的数组,以前的每个版本都只是一个快照,所以selectedItem拿到的是以前一个快照,而不是目前的版本!
尽管你也可以更新 selectedItem,但更简单的解决方法是消除重复项。在下面这个例子中,你将 selectedId 保存在 state 中,而不是在 selectedItem 对象中(它创建了一个与 items 内重复的对象),然后 通过搜索 items 数组中具有该 ID 的项,以此获取 selectedItem:
import { useState } from 'react';
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
export default function Menu() {
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);
const selectedItem = items.find(item =>
item.id === selectedId
);
function handleItemChange(id, e) {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
title: e.target.value,
};
} else {
return item;
}
}));
}
return (
<>
...
</>
);
}
state 过去常常是这样复制的:
items = [{ id: 0, title: 'pretzels'}, ...]selectedItem = {id: 0, title: 'pretzels'}
改了之后是这样的:
items = [{ id: 0, title: 'pretzels'}, ...]selectedId = 0
重复的 state 没有了,你只保留了必要的 state!
现在,如果你编辑 selected 元素,下面的消息将立即更新。这是因为 setItems 会触发重新渲染,而 items.find(...) 会找到带有更新文本的元素。你不需要在 state 中保存 选定的元素,因为只有 选定的 ID 是必要的。其余的可以在渲染期间计算。
避免深度嵌套的 state
想象一下,一个由行星、大陆和国家组成的旅行计划。你可能会尝试使用嵌套对象和数组来构建它的 state,就像下面这个例子:
export const initialTravelPlan = {
id: 0,
title: '(Root)',
childPlaces: [{
id: 1,
title: 'Earth',
childPlaces: [{
id: 2,
title: 'Africa',
childPlaces: [{
id: 3,
title: 'Botswana',
childPlaces: []
}]
}]
}]
};
现在,假设你想添加一个按钮来删除一个你已经去过的地方。你会怎么做呢?更新嵌套的 state 需要从更改部分一直向上复制对象。删除一个深度嵌套的地点将涉及复制其整个父级地点链。这样的代码可能非常冗长。
如果 state 嵌套太深,难以轻松更新,可以考虑将其“扁平化”。 这里有一个方法可以重构上面这个数据。不同于树状结构,每个节点的 place 都是一个包含 其子节点 的数组,你可以让每个节点的 place 作为数组保存 其子节点的 ID。然后存储一个节点 ID 与相应节点的映射关系。
这个数据重组可能会让你想起看到一个数据库表:
export const initialTravelPlan = {
0: {
id: 0,
title: '(Root)',
childIds: [1, 42, 46],
},
1: {
id: 1,
title: 'Earth',
childIds: [2, 10, 19, 26, 34]
},
2: {
id: 2,
title: 'Africa',
childIds: [3, 4, 5, 6 , 7, 8, 9]
},
3: {
id: 3,
title: 'Botswana',
childIds: []
}
};
现在 state 已经“扁平化”(也称为“规范化”),更新嵌套项会变得更加容易。
现在要删除一个地点,你只需要更新两个 state 级别:
- 其 父级 地点的更新版本应该从其
childIds数组中排除已删除的 ID。 - 其根级“表”对象的更新版本应包括父级地点的更新版本。
下面是展示如何处理它的一个示例:
import { useState } from 'react';
import { initialTravelPlan } from './places.js';
export default function TravelPlan() {
const [plan, setPlan] = useState(initialTravelPlan);
function handleComplete(parentId, childId) {
const parent = plan[parentId];
// 创建一个其父级地点的新版本
// 但不包括子级 ID。
const nextParent = {
...parent,
childIds: parent.childIds
.filter(id => id !== childId)
};
// 更新根 state 对象...
setPlan({
...plan,
// ...以便它拥有更新的父级。
[parentId]: nextParent
});
}
const root = plan[0];
const planetIds = root.childIds;
return (
<>
...
</>
);
}你确实可以随心所欲地嵌套 state,但是将其“扁平化”可以解决许多问题。这使得 state 更容易更新,并且有助于确保在嵌套对象的不同部分中没有重复。
在组件间共享状态
有时候,你希望两个组件的状态始终同步更改。要实现这一点,可以将相关 state 从这两个组件上移除,并把 state 放到它们的公共父级,再通过 props 将 state 传递给这两个组件。这被称为“状态提升”,这是编写 React 代码时常做的事。
举例说明一下状态提升
在这个例子中,父组件 Accordion 渲染了 2 个独立的 Panel 组件。
AccordionPanelPanel
每个 Panel 组件都有一个布尔值 isActive,用于控制其内容是否可见。
import { useState } from 'react';
function Panel({ title, children }) {
const [isActive, setIsActive] = useState(false);
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={() => setIsActive(true)}>
显示
</button>
)}
</section>
);
}
export default function Accordion() {
return (
<>
<h2>哈萨克斯坦,阿拉木图</h2>
<Panel title="关于">
阿拉木图人口约200万,是哈萨克斯坦最大的城市。
</Panel>
<Panel title="词源">
这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思。
</Panel>
</>
);
}
我们发现点击其中一个面板中的按钮并不会影响另外一个,他们是独立的。
假设现在你想改变这种行为,以便在任何时候只展开一个面板。在这种设计下,展开第 2 个面板应会折叠第 1 个面板。你该如何做到这一点呢?“
要协调好这两个面板,我们需要分 3 步将状态“提升”到他们的父组件中。
- 从子组件中 移除 state 。
- 从父组件 传递 硬编码数据。
- 为共同的父组件添加 state ,并将其与事件处理函数一起向下传递。
这样,Accordion 组件就可以控制 2 个 Panel 组件,保证同一时间只能展开一个。
第 1 步: 从子组件中移除状态
你将把 Panel 组件对 isActive 的控制权交给他们的父组件。这意味着,父组件会将 isActive 作为 prop 传给子组件 Panel。我们先从 Panel 组件中 删除下面这一行:
const [isActive, setIsActive] = useState(false);然后,把 isActive 加入 Panel 组件的 props 中:
function Panel({ title, children, isActive }) {现在 Panel 的父组件就可以通过 向下传递 prop 来 控制 isActive。但相反地,Panel 组件对 isActive 的值 没有控制权 —— 现在完全由父组件决定!
第 2 步: 从公共父组件传递硬编码数据
为了实现状态提升,必须定位到你想协调的 两个 子组件最近的公共父组件:
Accordion(最近的公共父组件)PanelPanel
在这个例子中,公共父组件是 Accordion。因为它位于两个面板之上,可以控制它们的 props,所以它将成为当前激活面板的“控制之源”。通过 Accordion 组件将硬编码值 isActive(例如 true )传递给两个面板:
import { useState } from 'react';
export default function Accordion() {
return (
<>
<h2>哈萨克斯坦,阿拉木图</h2>
<Panel title="关于" isActive={true}>
阿拉木图人口约200万,是哈萨克斯坦最大的城市。
</Panel>
<Panel title="词源" isActive={true}>
这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思。
</Panel>
</>
);
}
function Panel({ title, children, isActive }) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={() => setIsActive(true)}>
显示
</button>
)}
</section>
);
}
你可以尝试修改 Accordion 组件中 isActive 的值,并在屏幕上查看结果。
第 3 步: 为公共父组件添加状态
状态提升通常会改变原状态的数据存储类型。
在这个例子中,一次只能激活一个面板。这意味着 Accordion 这个父组件需要记录 哪个 面板是被激活的面板。我们可以用数字作为当前被激活 Panel 的索引,而不是 boolean 值:
const [activeIndex, setActiveIndex] = useState(0);当 activeIndex 为 0 时,激活第一个面板,为 1 时,激活第二个面板。
在任意一个 Panel 中点击“显示”按钮都需要更改 Accordion 中的激活索引值。 Panel 中无法直接设置状态 activeIndex 的值,因为该状态是在 Accordion 组件内部定义的。 Accordion 组件需要 显式允许 Panel 组件通过 将事件处理程序作为 prop 向下传递 来更改其状态:
<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
...
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
...
</Panel>
</>现在 Panel 组件中的 <button> 将使用 onShow 这个属性作为其点击事件的处理程序:
import { useState } from 'react';
export default function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<h2>哈萨克斯坦,阿拉木图</h2>
<Panel
title="关于"
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
阿拉木图人口约200万,是哈萨克斯坦最大的城市。
</Panel>
<Panel
title="词源"
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思。
</Panel>
</>
);
}
function Panel({
title,
children,
isActive,
onShow
}) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={onShow}>
显示
</button>
)}
</section>
);
}
这样,我们就完成了对状态的提升!将状态移至公共父组件中可以让你更好的管理这两个面板。使用激活索引值代替之前的 是否显示 标识确保了一次只能激活一个面板。而通过向下传递事件处理函数可以让子组件修改父组件的状态。

受控组件和非受控组件
通常我们把包含“不受控制”状态的组件称为“非受控组件”。例如,最开始带有
isActive状态变量的Panel组件就是不受控制的,因为其父组件无法控制面板的激活状态。相反,当组件中的重要信息是由
props而不是其自身状态驱动时,就可以认为该组件是“受控组件”。这就允许父组件完全指定其行为。最后带有isActive属性的Panel组件是由Accordion组件控制的。当编写一个组件时,你应该考虑哪些信息应该受控制(通过
props),哪些信息不应该受控制(通过state)。当然,你可以随时改变主意并重构代码。
每个状态都对应唯一的数据源
在 React 应用中,很多组件都有自己的状态。一些状态可能“活跃”在叶子组件(树形结构最底层的组件)附近,例如输入框。另一些状态可能在应用程序顶部“活动”。例如,客户端路由库也是通过将当前路由存储在 React 状态中,利用 props 将状态层层传递下去来实现的!
对于每个独特的状态,都应该存在且只存在于一个指定的组件中作为 state。这一原则也被称为拥有 “可信单一数据源”。它并不意味着所有状态都存在一个地方——对每个状态来说,都需要一个特定的组件来保存这些状态信息。你应该 将状态提升 到公共父级,或 将状态传递 到需要它的子级中,而不是在组件之间复制共享的状态。
你的应用会随着你的操作而变化。当你将状态上下移动时,你依然会想要确定每个状态在哪里“活跃”。这都是过程的一部分!
对 state 进行保留和重置
各个组件的 state 是各自独立的。根据组件在 UI 树中的位置,React 可以跟踪哪些 state 属于哪个组件。你可以控制在重新渲染过程中何时对 state 进行保留和重置。
状态与渲染树中的位置相关
React 会为 UI 中的组件结构构建渲染树。
当向一个组件添加状态时,那么可能会认为状态“存在”在组件内。但实际上,状态是由 React 保存的。React 通过组件在渲染树中的位置将它保存的每个状态与正确的组件关联起来。
下面的例子中只有一个 <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 (
);
}
下面是它们的树形结构的样子:

这是两个独立的 counter,因为它们在树中被渲染在了各自的位置。 一般情况下你不用去考虑这些位置来使用 React,但知道它们是如何工作会很有用。
在 React 中,屏幕中的每个组件都有完全独立的 state。举个例子,当你并排渲染两个 Counter 组件时,它们都会拥有各自独立的 score 和 hover state。
试试点击两个 counter 你会发现它们互不影响:
import { useState } from 'react';
export default function App() {
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 (
);
}
如你所见,当一个计数器被更新时,只有该组件的状态会被更新:

只有当在树中相同的位置渲染相同的组件时,React 才会一直保留着组件的 state。想要验证这一点,可以将两个计数器的值递增,取消勾选 “渲染第二个计数器” 复选框,然后再次勾选它:
import { useState } from 'react';
export default function App() {
const [showB, setShowB] = useState(true);
return (
<div>
<Counter />
{showB && <Counter />}
<label>
<input
type="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>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
使用好看的样式
</label>
</div>
);
}当你勾选或清空复选框的时候,计数器 state 并没有被重置。不管 isFancy 是 true 还是 false,根组件 App 返回的 div 的第一个子组件都是 <Counter />:

它是位于相同位置的相同组件,所以对 React 来说,它是同一个计数器。
提示
记住 对 React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置!
React 不知道你的函数里是如何进行条件判断的,它只会“看到”你返回的树。
相同位置的不同组件会使 state 重置
在这个例子中,勾选复选框会将 <Counter> 替换为一个 <p>:
import { useState } from 'react';
export default function App() {
const [isPaused, setIsPaused] = useState(false);
return (
<div>
{isPaused ? (
<p>待会见!</p>
) : (
<Counter />
)}
<label>
<input
type="checkbox"
checked={isPaused}
onChange={e => {
setIsPaused(e.target.checked)
}}
/>
休息一下
</label>
</div>
);
}示例中,你在相同位置对 不同 的组件类型进行切换。刚开始 <div> 的第一个子组件是一个 Counter。但是当你切换成 p 时,React 将 Counter 从 UI 树中移除了并销毁了它的状态。


并且,当你在相同位置渲染不同的组件时,组件的整个子树都会被重置。
当你勾选复选框后计数器的 state 被重置了。虽然你渲染了一个 Counter,但是 div 的第一个子组件从 section 变成了 div。当子组件 section 从 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 (
<input
value={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。
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>
);
}目前当你切换玩家时,分数会被保留下来。这两个 Counter 出现在相同的位置,所以 React 会认为它们是 同一个 Counter,只是传了不同的 person prop。
但是从概念上讲,这个应用中的两个计数器应该是各自独立的。虽然它们在 UI 中的位置相同,但是一个是 Taylor 的计数器,一个是 Sarah 的计数器。
有两个方法可以在它们相互切换时重置 state:
- 将组件渲染在不同的位置
- 使用
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。

重要
React 的 diff 不是「先渲染出最终可见的 DOM 树,再去对比那棵树长什么样」,而是直接对比两次渲染函数返回的 JSX 元素树本身——包括那些渲染结果是 false/null 的”空位”。
在你这段代码里,<div> 的子节点实际上是一个长度固定为 3 的数组(按 JSX 里出现的位置排好):
- 位置 0:
{isPlayerA && <Counter person="Taylor" />} - 位置 1:
{!isPlayerA && <Counter person="Sarah" />} - 位置 2:
<button>...</button>
isPlayerA = true 时数组是 [<Counter Taylor/>, false, <button/>];
isPlayerA = false 时数组是 [false, <Counter Sarah/>, <button/>]。
每当 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。
提示
请记住 key 不是全局唯一的。它们只能指定父组件内部的顺序。
迁移状态逻辑至 Reducer 中
对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,你可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。
在我看来,
reducer就是把特定的state以及相对应的逻辑聚合在一起变为一个新的自定义的state(hook)
使用 reducer 整合状态逻辑
随着组件复杂度的增加,你将很难一眼看清所有的组件状态更新逻辑。例如,下面的 TaskApp 组件有一个数组类型的状态 tasks,并通过三个不同的事件处理程序来实现任务的添加、删除和修改:
import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
export default function TaskApp() {
const [tasks, setTasks] = useState(initialTasks);
//添加动作
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
//更新动作
function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}
//删除动作
function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}
return (
<>
...
</>
);
}这个组件的每个事件处理程序都通过 setTasks 来更新状态。随着这个组件的不断迭代,其状态逻辑也会越来越多。为了降低这种复杂度,并让所有逻辑都可以存放在一个易于理解的地方,你可以将这些状态逻辑移到组件之外的一个称为 reducer 的函数中。
Reducer 是处理状态的另一种方式。你可以通过三个步骤将 useState 迁移到 useReducer:
- 将设置状态的逻辑 修改 成 dispatch 的一个 action;
- 编写 一个 reducer 函数;
- 在你的组件中 使用 reducer。
第 1 步: 将设置状态的逻辑修改成 dispatch 的一个 action
你的事件处理程序目前是通过设置状态来 实现逻辑的:
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}移除所有的状态设置逻辑。只留下三个事件处理函数:
handleAddTask(text)在用户点击 “添加” 时被调用。handleChangeTask(task)在用户切换任务或点击 “保存” 时被调用。handleDeleteTask(taskId)在用户点击 “删除” 时被调用。
使用 reducer 管理状态与直接设置状态略有不同。它不是通过设置状态来告诉 React “要做什么”,而是通过事件处理程序 dispatch 一个 “action” 来指明 “用户刚刚做了什么”。(而状态更新逻辑则保存在其他地方!)因此,我们不再通过事件处理器直接 “设置 task”,而是 dispatch 一个 “添加/修改/删除任务” 的 action。这更加符合用户的思维。
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}你传递给 dispatch 的对象叫做 “action”:
function handleDeleteTask(taskId) {
dispatch(
// "action" 对象:
{
type: 'deleted',
id: taskId,
}
);
}它是一个普通的 JavaScript 对象。它的结构是由你决定的,但通常来说,它应该至少包含可以表明 发生了什么事情 的信息。
第 2 步: 编写一个 reducer 函数
reducer 函数就是你放置状态逻辑的地方。它接受两个参数,分别为当前 state 和 action 对象,并且返回的是更新后的 state:
function yourReducer(state, action) {
// 给 React 返回更新后的状态
}React 会将状态设置为你从 reducer 返回的状态。
在这个例子中,要将状态设置逻辑从事件处理程序移到 reducer 函数中,你需要:
- 声明当前状态(
tasks)作为第一个参数; - 声明
action对象作为第二个参数; - 从
reducer返回 下一个 状态(React 会将旧的状态设置为这个最新的状态)。
下面是所有迁移到 reducer 函数的状态设置逻辑:
function tasksReducer(tasks, action) {
if (action.type === 'added') {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
} else if (action.type === 'changed') {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === 'deleted') {
return tasks.filter((t) => t.id !== action.id);
} else {
throw Error('未知 action: ' + action.type);
}
}由于 reducer 函数接受 state(tasks)作为参数,因此你可以 在组件之外声明它。这减少了代码的缩进级别,提升了代码的可读性。
第 3 步: 在组件中使用 reducer
最后,你需要将 tasksReducer 导入到组件中。记得先从 React 中导入 useReducer Hook:
import { useReducer } from 'react';接下来,你就可以替换掉之前的 useState:
const [tasks, setTasks] = useState(initialTasks);只需要像下面这样使用 useReducer:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);useReducer 和 useState 很相似——你必须给它传递一个初始状态,它会返回一个有状态的值和一个设置该状态的函数(在这个例子中就是 dispatch 函数)。但是,它们两个之间还是有点差异的。
useReducer 钩子接受 2 个参数:
- 一个 reducer 函数
- 一个初始的 state
它返回如下内容:
- 一个有状态的值
- 一个 dispatch 函数(用来 “派发” 用户操作给 reducer)
现在一切都准备就绪了!我们在这里把 reducer 定义在了组件的末尾:
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}
return (
<>
...
</>
);
}
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'changed': {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error('未知 action: ' + action.type);
}
}
}如果有需要,你甚至可以把 reducer 移到一个单独的文件中:
app.js
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import tasksReducer from './tasksReducer.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}
return (
<>
...
</>
);
}tasksReducer.js
export default function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'changed': {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error('未知 action:' + action.type);
}
}
}
当像这样分离关注点时,我们可以更容易地理解组件逻辑。现在,事件处理程序只通过派发 action 来指定 发生了什么,而 reducer 函数通过响应 actions 来决定 状态如何更新。
编写一个好的 reducer
编写 reducer 时最好牢记以下两点:
- reducer 必须是纯粹的。 这一点和状态更新函数是相似的,
reducer是在渲染时运行的!(actions 会排队直到下一次渲染)。 这就意味着reducer必须纯净,即当输入相同时,输出也是相同的。它们不应该包含异步请求、定时器或者任何副作用(对组件外部有影响的操作)。它们应该以不可变值的方式去更新 对象 和 数组。 - 每个 action 都描述了一个单一的用户交互,即使它会引发数据的多个变化。
使用 Context 深层传递参数
通常来说,你会通过 props 将信息从父组件传递到子组件。但是,如果你必须通过许多中间组件向下传递 props,或是在你应用中的许多组件需要相同的信息,传递 props 会变的十分冗长和不便。Context 允许父组件向其下层无论多深的任何组件提供信息,而无需通过 props 显式传递。
传递 props 带来的问题
传递props是将数据通过 UI 树显式传递到使用它的组件的好方法。
但是当你需要在组件树中深层传递参数以及需要在组件间复用相同的参数时,传递 props 就会变得很麻烦。最近的共同祖先组件可能离需要数据的组件很远,状态提升到太高的层级会导致 “逐层传递 props” 的情况。

要是有一种方法可以在组件树中不需要 props 将数据“直达”到所需的组件中,那可就太好了。React 的 context 功能可以满足我们的这个心愿。
Context:传递 props 的另一种方法
Context 让父组件可以为它下面的整个组件树提供数据。Context 有很多种用途。这里就有一个示例。思考一下这个 Heading 组件接收一个 level 参数来决定它标题尺寸的场景:
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section>
<Heading level={1}>主标题</Heading>
<Heading level={2}>副标题</Heading>
<Heading level={3}>子标题</Heading>
<Heading level={4}>子子标题</Heading>
<Heading level={5}>子子子标题</Heading>
<Heading level={6}>子子子子标题</Heading>
</Section>
);
}
假设你想让相同 Section 中的多个 Heading 具有相同的尺寸:
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section>
<Heading level={1}>主标题</Heading>
<Section>
<Heading level={2}>副标题</Heading>
<Heading level={2}>副标题</Heading>
<Heading level={2}>副标题</Heading>
<Section>
<Heading level={3}>子标题</Heading>
<Heading level={3}>子标题</Heading>
<Heading level={3}>子标题</Heading>
<Section>
<Heading level={4}>子子标题</Heading>
<Heading level={4}>子子标题</Heading>
<Heading level={4}>子子标题</Heading>
</Section>
</Section>
</Section>
</Section>
);
}
目前,你将 level 参数分别传递给每个 <Heading>:
<Section>
<Heading level={3}>关于</Heading>
<Heading level={3}>照片</Heading>
<Heading level={3}>视频</Heading>
</Section>将 level 参数传递给 <Section> 组件而不是传给 <Heading> 组件看起来更好一些。这样的话你可以强制使同一个 section 中的所有标题都有相同的尺寸:
<Section level={3}>
<Heading>关于</Heading>
<Heading>照片</Heading>
<Heading>视频</Heading>
</Section>但是 <Heading> 组件是如何知道离它最近的 <Section> 的 level 的呢?这需要子组件可以通过某种方式“访问”到组件树中某处在其上层的数据。
你不能只通过 props 来实现它。这就是 context 大显身手的地方。你可以通过以下三个步骤来实现它:
- 创建 一个 context。(你可以将其命名为
LevelContext, 因为它表示的是标题级别。) - 在需要数据的组件内 使用 刚刚创建的 context。(
Heading将会使用LevelContext。) - 在指定数据的组件中 提供 这个 context。(
Section将会提供LevelContext。)
Context 可以让父节点,甚至是很远的父节点都可以为其内部的整个组件树提供数据。

Step 1:创建 context
首先,你需要创建这个 context,并 将其从一个文件中导出,这样你的组件才可以使用它:
LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(1);createContext 只需默认值这么一个参数。在这里, 1 表示最大的标题级别,但是你可以传递任何类型的值(甚至可以传入一个对象)。你将在下一个步骤中见识到默认值的意义。
Step 2:使用 Context
从 React 中引入 useContext Hook 以及你刚刚创建的 context:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';目前,Heading 组件从 props 中读取 level:
export default function Heading({ level, children }) {
// ...
}删掉 level 参数并从你刚刚引入的 LevelContext 中读取值:
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}useContext 是一个 Hook。和 useState 以及 useReducer一样,你只能在 React 组件中(不是循环或者条件里)立即调用 Hook。useContext 告诉 React Heading 组件想要读取 LevelContext。
现在 Heading 组件没有 level 参数,你不需要再像这样在你的 JSX 中将 level 参数传递给 Heading:
<Section>
<Heading level={4}>子子标题</Heading>
<Heading level={4}>子子标题</Heading>
<Heading level={4}>子子标题</Heading>
</Section>修改一下 JSX,让 Section 组件代替 Heading 组件接收 level 参数:
<Section level={4}>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
</Section>注意!这个示例还不能运行。所有 headings 的尺寸都一样,因为 即使你正在使用 context,但是你还没有提供它。 React 不知道从哪里获取这个 context!
如果你不提供 context,React 会使用你指定的默认值。在这个例子中,你为 createContext 传入了 1 这个参数,所以 useContext(LevelContext) 会返回 1,把所有的标题都设置为<h1>。我们通过让每个 Section 提供它自己的 context 来修复这个问题。
Step 3:提供 context
Section 组件目前渲染传入它的子组件:
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}把它们用 context provider 包裹起来 以提供 LevelContext 给它们:
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext value={level}>
{children}
</LevelContext>
</section>
);
}这告诉 React:“如果在 <Section> 组件中的任何子组件请求 LevelContext,给他们这个 level。”组件会使用 UI 树中在它上层最近的那个 <LevelContext> 传递过来的值。
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section level={1}>
<Heading>主标题</Heading>
<Section level={2}>
<Heading>副标题</Heading>
<Heading>副标题</Heading>
<Heading>副标题</Heading>
<Section level={3}>
<Heading>子标题</Heading>
<Heading>子标题</Heading>
<Heading>子标题</Heading>
<Section level={4}>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
</Section>
</Section>
</Section>
</Section>
);
}这与原始代码的运行结果相同,但是你不需要向每个 Heading 组件传递 level 参数了!取而代之的是,它通过访问上层最近的 Section 来“断定”它的标题级别:
- 你将一个
level参数传递给<Section>。 Section把它的子元素包在<LevelContext value={level}>里面。Heading使用useContext(LevelContext)访问上层最近的LevelContext提供的值。
在相同的组件中使用并提供 context
目前,你仍需要手动指定每个 section 的 level:
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...由于 context 让你可以从上层的组件读取信息,每个 Section 都会从上层的 Section 读取 level,并自动向下层传递 level + 1。 你可以像下面这样做:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext value={level + 1}>
{children}
</LevelContext>
</section>
);
}这样修改之后,你不用将 level 参数传给 <Section> 或者是 <Heading> 了:
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section>
<Heading>主标题</Heading>
<Section>
<Heading>副标题</Heading>
<Heading>副标题</Heading>
<Heading>副标题</Heading>
<Section>
<Heading>子标题</Heading>
<Heading>子标题</Heading>
<Heading>子标题</Heading>
<Section>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
</Section>
</Section>
</Section>
</Section>
);
}
section.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext value={level + 1}>
{children}
</LevelContext>
</section>
);
}
现在,Heading 和 Section 都通过读取 LevelContext 来判断它们的深度。而且 Section 把它的子组件都包在 LevelContext 中来指定其中的任何内容都处于一个“更深”的级别。
提示
本示例使用标题级别来展示,因为它们直观地显示了嵌套组件如何覆盖 context。但是 context 对于许多其他的场景也很有用。你可以用它来传递整个子树需要的任何信息:当前的颜色主题、当前登录的用户等。
Context 会穿过中间层级的组件
你可以在提供 context 的组件和使用它的组件之间的层级插入任意数量的组件。这包括像 <div> 这样的内置组件和你自己创建的组件。
写在你使用 context 之前
使用 Context 看起来非常诱人!然而,这也意味着它也太容易被过度使用了。如果你只想把一些 props 传递到多个层级中,这并不意味着你需要把这些信息放到 context 里。
在使用 context 之前,你可以考虑以下几种替代方案:
- 从 传递 props 开始。 如果你的组件看起来不起眼,那么通过十几个组件向下传递一堆 props 并不罕见。这有点像是在埋头苦干,但是这样做可以让哪些组件用了哪些数据变得十分清晰!维护你代码的人会很高兴你用 props 让数据流变得更加清晰。
- 抽象组件并 将 JSX 作为
children传递 给它们。(能减少层级) 如果你通过很多层不使用该数据的中间组件(并且只会向下传递)来传递数据,这通常意味着你在此过程中忘记了抽象组件。举个例子,你可能想传递一些像posts的数据 props 到不会直接使用这个参数的组件,类似<Layout posts={posts} />。取而代之的是,让Layout把children当做一个参数,然后渲染<Layout><Posts posts={posts} /></Layout>。这样就减少了定义数据的组件和使用数据的组件之间的层级。
如果这两种方法都不适合你,再考虑使用 context。
Context 的使用场景
- 主题: 如果你的应用允许用户更改其外观(例如暗夜模式),你可以在应用顶层放一个 context provider,并在需要调整其外观的组件中使用该 context。
- 当前账户: 许多组件可能需要知道当前登录的用户信息。将它放到 context 中可以方便地在树中的任何位置读取它。某些应用还允许你同时操作多个账户(例如,以不同用户的身份发表评论)。在这些情况下,将 UI 的一部分包裹到具有不同账户数据的 provider 中会很方便。
- 路由: 大多数路由解决方案在其内部使用 context 来保存当前路由。这就是每个链接“知道”它是否处于活动状态的方式。如果你创建自己的路由库,你可能也会这么做。
- 状态管理: 随着你的应用的增长,最终在靠近应用顶部的位置可能会有很多 state。许多遥远的下层组件可能想要修改它们。
Context 不局限于静态值。如果你在下一次渲染时传递不同的值,React 将会更新读取它的所有下层组件!这就是 context 经常和 state 结合使用的原因。
一般而言,如果树中不同部分的远距离组件需要某些信息,context 将会对你大有帮助。
使用 Reducer 和 Context 拓展你的应用
Reducer 可以整合组件的状态更新逻辑。Context 可以将信息深入传递给其他组件。你可以组合使用它们来共同管理一个复杂页面的状态。
也就是通过
context将reducer向下传递,叶子组件不需要关心数据来源以及数据处理细节。
结合使用 reducer 和 context
在 reducer 介绍 的例子里面,状态被 reducer 所管理。reducer 函数包含了所有的状态更新逻辑并在此文件的底部声明:
TaskApp.js
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
return (
<>
...
</>
);
}
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
Reducer 有助于保持事件处理程序的简短明了。但随着应用规模越来越庞大,你就可能会遇到别的困难。目前,tasks 状态和 dispatch 函数仅在顶级 TaskApp 组件中可用。要让其他组件读取任务列表或更改它,你必须显式 传递 当前状态和事件处理程序,将其作为 props。
例如,TaskApp 将 一系列 task 和事件处理程序传递给 TaskList:
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>TaskList 将事件处理程序传递给 Task:
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>在像这样的小示例里这样做没什么问题,但是如果你有成百上千个中间组件,传递所有状态和函数可能会非常麻烦!
这就是为什么,比起通过 props 传递它们,你可能想把 tasks 状态和 dispatch 函数都 放入 context。这样,所有的在 TaskApp 组件树之下的组件都不必一直往下传 props 而可以直接读取 tasks 和 dispatch 函数。
下面将介绍如何结合使用 reducer 和 context:
- 创建 context。
- 将 state 和 dispatch 放入 context。
- 在组件树的任何地方 使用 context。
第一步: 创建 context
useReducer 返回当前的 tasks 和 dispatch 函数来让你更新它们:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);为了将它们从组件树往下传,你将创建两个不同的 context:
TasksContext提供当前的 tasks 列表。TasksDispatchContext提供了一个函数可以让组件分发动作。
将它们从单独的文件导出,以便以后可以从其他文件导入它们:
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);在这里,你把 null 作为默认值传递给两个 context。实际值是由 TaskApp 组件提供的。
第二步: 将 state 和 dispatch 函数放入 context
现在,你可以将所有的 context 导入 TaskApp 组件。获取 useReducer() 返回的 tasks 和 dispatch 并将它们 提供 给整个组件树:
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
...
</TasksDispatchContext>
</TasksContext>
);
}现在,你可以同时通过 props 和 context 传递信息:
TaskApp.js
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</TasksDispatchContext>
</TasksContext>
);
}
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}在下一步中,你将删除通过 props 传递的代码。
Step 3: 在组件树中的任何地方使用 context
现在你不需要将 tasks 和事件处理程序在组件树中传递:
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext>
</TasksContext>相反,任何需要 tasks 的组件都可以从 TasksContext 中读取它:
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...任何组件都可以从 context 中读取 dispatch 函数并调用它,从而更新任务列表:
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
// ...TaskApp 组件不会向下传递任何事件处理程序,TaskList 也不会。每个组件都会读取它需要的 context:
import { useState, useContext } from 'react';
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskList() {
const tasks = useContext(TasksContext);
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task task={task} />
</li>
))}
</ul>
);
}
function Task({ task }) {
const [isEditing, setIsEditing] = useState(false);
const dispatch = useContext(TasksDispatchContext);
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={e => {
dispatch({
type: 'changed',
task: {
...task,
text: e.target.value
}
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
taskContent = (
<>
{task.text}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={task.done}
onChange={e => {
dispatch({
type: 'changed',
task: {
...task,
done: e.target.checked
}
});
}}
/>
{taskContent}
<button onClick={() => {
dispatch({
type: 'deleted',
id: task.id
});
}}>
Delete
</button>
</label>
);
}
state 仍然 “存在于” 顶层 Task 组件中,由 useReducer 进行管理。不过,组件树里的组件只要导入这些 context 之后就可以获取 tasks 和 dispatch。
将相关逻辑迁移到一个文件当中
这不是必须的,但你可以通过将 reducer 和 context 移动到单个文件中来进一步整理组件。目前,“TasksContext.js” 仅包含两个 context 声明:
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);来给这个文件添加更多代码!将 reducer 移动到此文件中,然后声明一个新的 TasksProvider 组件。此组件将所有部分连接在一起:
- 它将管理 reducer 的状态。
- 它将提供现有的 context 给组件树。
- 它将 把
children作为 prop,所以你可以传递 JSX。
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
{children}
</TasksDispatchContext>
</TasksContext>
);
}这将使 TaskApp 组件更加直观:
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';
export default function TaskApp() {
return (
<TasksProvider>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksProvider>
);
}
你也可以从 TasksContext.js 中导出使用 context 的函数:
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}组件可以通过以下函数读取 context:
const tasks = useTasks();
const dispatch = useTasksDispatch();这不会改变任何行为,但它会允许你之后进一步分割这些 context 或向这些函数添加一些逻辑。现在所有的 context 和 reducer 连接部分都在 TasksContext.js 中。这保持了组件的干净和整洁,让我们专注于它们显示的内容,而不是它们从哪里获得数据:
app.js
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';
export default function TaskApp() {
return (
<TasksProvider>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksProvider>
);
}
TasksContext.js
import { createContext, useContext, useReducer } from 'react';
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
{children}
</TasksDispatchContext>
</TasksContext>
);
}
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
const initialTasks = [
{ id: 0, text: 'Philosopher’s Path', done: true },
{ id: 1, text: 'Visit the temple', done: false },
{ id: 2, text: 'Drink matcha', done: false }
];
AddTask.js
import { useState } from 'react';
import { useTasksDispatch } from './TasksContext.js';
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useTasksDispatch();
return (
<>
<input
placeholder="Add task"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
</>
);
}
let nextId = 3;
TaskList.js
import { useState } from 'react';
import { useTasks, useTasksDispatch } from './TasksContext.js';
export default function TaskList() {
const tasks = useTasks();
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task task={task} />
</li>
))}
</ul>
);
}
function Task({ task }) {
const [isEditing, setIsEditing] = useState(false);
const dispatch = useTasksDispatch();
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={e => {
dispatch({
type: 'changed',
task: {
...task,
text: e.target.value
}
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
taskContent = (
<>
{task.text}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={task.done}
onChange={e => {
dispatch({
type: 'changed',
task: {
...task,
done: e.target.checked
}
});
}}
/>
{taskContent}
<button onClick={() => {
dispatch({
type: 'deleted',
id: task.id
});
}}>
Delete
</button>
</label>
);
}
你可以将 TasksProvider 视为页面的一部分,它知道如何处理 tasks。useTasks 用来读取它们,useTasksDispatch 用来从组件树下的任何组件更新它们。
提示
像 useTasks 和 useTasksDispatch 这样的函数被称为 自定义 Hook。 如果你的函数名以 use 开头,它就被认为是一个自定义 Hook。这让你可以使用其他 Hook,比如 useContext。
随着应用的增长,你可能会有许多这样的 context 和 reducer 的组合。这是一种强大的拓展应用并提升状态的方式,让你在组件树深处访问数据时无需进行太多工作。
评论
评论加载中……