React系列五:状态管理

React系列224429 阅读0

用 State 响应输入

React 控制 UI 的方式是声明式的。你不必直接控制 UI 的各个部分,只需要声明组件可以处于的不同状态,并根据用户的输入在它们之间切换。这与设计师对 UI 的思考方式很相似。

通俗的说就是根据一个状态来判断展示哪一个组件。

选择 State 结构

React推荐使用尽量简单但是意义明确的state:

  1. 如果两个 state 变量总是一起更新,请考虑将它们合并为一个。
  2. 仔细选择你的 state 变量,以避免创建“极难处理”的 state。
  3. 用一种减少出错更新的机会的方式来构建你的 state。
  4. 避免冗余和重复的 state,这样你就不需要保持同步。
  5. 除非你特别想防止更新,否则不要将 props 放入 state 中。
  6. 对于选择类型的 UI 模式,请在 state 中保存 ID 或索引而不是对象本身。
  7. 如果深度嵌套 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 ToolkitZustand

评论

发表评论