React系列五:状态管理
用 State 响应输入
React 控制 UI 的方式是声明式的。你不必直接控制 UI 的各个部分,只需要声明组件可以处于的不同状态,并根据用户的输入在它们之间切换。这与设计师对 UI 的思考方式很相似。
通俗的说就是根据一个状态来判断展示哪一个组件。
选择 State 结构
React推荐使用尽量简单但是意义明确的state:
- 如果两个 state 变量总是一起更新,请考虑将它们合并为一个。
- 仔细选择你的 state 变量,以避免创建“极难处理”的 state。
- 用一种减少出错更新的机会的方式来构建你的 state。
- 避免冗余和重复的 state,这样你就不需要保持同步。
- 除非你特别想防止更新,否则不要将 props 放入 state 中。
- 对于选择类型的 UI 模式,请在 state 中保存 ID 或索引而不是对象本身。
- 如果深度嵌套 state 更新很复杂,请尝试将其展开扁平化。
在组件间共享状态
有时候,你希望两个组件的状态始终同步更改。要实现这一点,可以将相关 state 从这两个组件上移除,并把 state 放到它们的公共父级,再通过 props 将 state 传递给这两个组件。这被称为“状态提升”,这是编写 React 代码时常做的事。
对 state 进行保留和重置
状态保留
我们知道,state在各个组件中保持独立,能保存各自的状态互不干扰。
但是React 在移除一个组件时,也会销毁它的 state。重新渲染的话,这个组件的state就会被初始化。但有时候我们希望在销毁组件的时候也保留这个state。
React 会为 UI 中的组件结构构建 渲染树。
只要一个组件还被渲染在 UI 树的相同位置,React 就会保留它的 state。 如果它被移除,或者一个不同的组件被渲染在相同的位置,那么 React 就会丢掉它的 state。
例如:
{isPlayerA ? (
<Counter person="Taylor" />
) : (
<Counter person="Sarah" />
)}
使用了if和else,是在组件树的相同位置,也是同一个组件,所以在isPlayerA切换时组件虽然重新渲染了,但是state仍会得以保留。
状态重置
默认情况下,React 会在一个组件保持在同一位置时保留它的 state。但有时候,你可能想要重置一个组件的 state。
方法一:将组件渲染在不同的位置
{isPlayerA &&
<Counter person="Taylor" />
}
{!isPlayerA &&
<Counter person="Sarah" />
}
这样,它们在组件树中就不在同一个位置。
方法二:使用 key 来重置 state
key 不只可以用于列表!你可以使用 key 来让 React 区分任何组件。
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
这样,在切换isPlayerA时,由于key不同,也被视为了两个组件,所以重置state
Reducer
对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,你可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。
Reducer 在概念和用途上与 Vuex、Pinia 是类似的,都是用于状态管理的机制。
以一个Todo程序为例:
// 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 (
<>
<h1>布拉格的行程安排</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{id: 0, text: '参观卡夫卡博物馆', done: true},
{id: 1, text: '看木偶戏', done: false},
{id: 2, text: '打卡列侬墙', done: false},
];
// tasksReducer.js
// 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);
}
}
}
tasksReducer就是我们的Reducer函数。
Reducer函数接受两个参数,分别为当前 state 和 action 对象,并且返回的是更新后的 state
在使用时,我们不再用UseState,而是使用useReducer:
// const [tasks, setTasks] = useState(initialTasks);
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer
useReducer 钩子接受 2 个参数:
一个 reducer 函数
一个初始的 state
它返回如下内容:
一个有状态的值
一个 dispatch 函数,用来 “派发” 用户操作(action)给 reducer。
在这个例子中,使用
dispatch({
type: 'added',
id: nextId++,
text: text,
});
时,useReducer会帮助你执行tasksReducer,并将参数传给useReducer的第二个参数action中。
编写 reducer 时最好牢记以下两点:
reducer 必须是纯粹的。 这一点和 状态更新函数 是相似的,reducer 是在渲染时运行的!(actions 会排队直到下一次渲染)。 这就意味着 reducer 必须纯净,即当输入相同时,输出也是相同的。它们不应该包含异步请求、定时器或者任何副作用(对组件外部有影响的操作)。它们应该以不可变值的方式去更新 对象 和 数组。
每个 action 都描述了一个单一的用户交互,即使它会引发数据的多个变化。 举个例子,如果用户在一个由 reducer 管理的表单(包含五个表单项)中点击了 重置按钮,那么 dispatch 一个 reset_form 的 action 比 dispatch 五个单独的 set_field 的 action 更加合理。如果你在一个 reducer 中打印了所有的 action 日志,那么这个日志应该是很清晰的,它能让你以某种步骤复现已发生的交互或响应。这对代码调试很有帮助!
注意
这里的reducer还不能直接用于全局状态管理,因为其他地方引用的时候得到的也是初始化过后的值,并且操作互不干扰。要使用还需要使用 React Context 配合,但是只适合小型项目,且缺少开发工具支持、模块化不如 Redux等插件。
在 React 中,最类似于 Vuex 或 Pinia 的状态管理方式有几个选择,但没有一个是“官方内置”的,都是社区维护或组合使用的。全局状态管理推荐使用 Redux Toolkit 或 Zustand 等
评论