代码下载
React官网已经都是函数式组件文档,没有类组件文档,但是还是支持类组件这种写法。
状态管理
React 会为 UI 中的组件结构构建 渲染树。当向一个组件添加状态时,那么可能会认为状态“存在”在组件内。但实际上,状态是由 React 保存的。React 通过组件在渲染树中的位置将它保存的每个状态与正确的组件关联起来。
只要一个组件还被渲染在 UI 树的相同位置,React 就会保留它的 state。 如果它被移除,或者一个不同的组件被渲染在相同的位置,那么 React 就会丢掉它的 state。
对 React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置!
不应该把组件函数的定义嵌套起来是因为每次渲染时都会创建一个 不同 的 组件函数。在相同位置渲染的是 不同 的组件,所以 React 将其下所有的 state 都重置了。这样会导致 bug 以及性能问题。
在相同位置重置 state,默认情况下,React 会在一个组件保持在同一位置时保留它的 state:
- 将组件渲染在不同的位置
- 使用 key 来重置 state
在 渲染列表 时见到过 key。但 key 不只可以用于列表!可以使用 key 来让 React 区分任何组件。默认情况下,React 使用父组件内部的顺序(“第一个组件”、“第二个组件”)来区分组件。但是 key 可以让你告诉 React 这不仅仅是 第一个 或者 第二个,而且还是一个特定的组件——例如,Taylor 的 计数器。这样无论它出现在树的任何位置, React 都会知道它是 Taylor 的 计数器!
useReducer
对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,你可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。
随着组件复杂度的增加,你将很难一眼看清所有的组件状态更新逻辑。例如,下面的 TaskApp 组件有一个数组类型的状态 tasks,并通过三个不同的事件处理程序来实现任务的添加、删除和修改:
let nextId = 3;
const initialTasks = [{id: 0, text: '参观卡夫卡博物馆', done: true},{id: 1, text: '看木偶戏', done: false},{id: 2, text: '打卡列侬墙', done: false},
];
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 (<><h4>布拉格的行程安排(useState 版本)</h4><AddTask onAddTask={handleAddTask} /><TaskListtasks={tasks}onChangeTask={handleChangeTask}onDeleteTask={handleDeleteTask}/></>);
}
为了降低这种复杂度,并让所有逻辑都可以存放在一个易于理解的地方,可以将这些状态逻辑移到组件之外的一个称为 reducer 的函数中。Reducer 是处理状态的另一种方式,可以通过三个步骤将 useState 迁移到 useReducer:
- 将设置状态的逻辑 修改成 dispatch 的一个 action;
- 编写 一个 reducer 函数;
- 在你的组件中 使用 reducer。
将设置状态的逻辑修改成 dispatch 的一个 action
移除所有的状态设置逻辑,只留下三个事件处理函数:
- handleAddTask(text) 在用户点击 “添加” 时被调用。
- handleChangeTask(task) 在用户切换任务或点击 “保存” 时被调用。
- handleDeleteTask(taskId) 在用户点击 “删除” 时被调用。
使用 reducers 管理状态与直接设置状态略有不同。它不是通过设置状态来告诉 React “要做什么”,而是通过事件处理程序 dispatch 一个 “action” 来指明 “用户刚刚做了什么”。(而状态更新逻辑则保存在其他地方!)因此不再通过事件处理器直接 “设置 task”,而是 dispatch 一个 “添加/修改/删除任务” 的 action。这更加符合用户的思维。
function handleAddTask(text) {dispatch({type: 'add',id: nextId++,text: text})}function handleChangeTask(task) {dispatch({type: 'change',task: task})}function handleDeleteTask(taskId) {dispatch({type: 'delete',id: taskId})}
传递给 dispatch 的对象叫做 “action”,它是一个普通的 JavaScript 对象。它的结构是由你决定的,但通常来说,它应该至少包含可以表明 发生了什么事情 的信息。通常会添加一个字符串类型的 type 字段来描述发生了什么,并通过其它字段传递额外的信息。
编写一个 reducer 函数
reducer 函数就是你放置状态逻辑的地方。它接受两个参数,分别为当前 state 和 action 对象,并且返回的是更新后的 state。React 会将状态设置为从 reducer 返回的状态。
要将状态设置逻辑从事件处理程序移到 reducer 函数中,需要:
- 声明当前状态(tasks)作为第一个参数;
- 声明 action 对象作为第二个参数;
- 从 reducer 返回 下一个 状态(React 会将旧的状态设置为这个最新的状态)。
function tasksReducer(tasks, action) {switch (action.type) {case 'add':return [...tasks,{id: action.id,text: action.text,done: false}]case 'change':return tasks.map((t) => t.id === action.id ? action.task : t)case 'delete':return tasks.filter((t) => t.id !== action.id)default:throw Error('未知错误:', action.type)}
在组件中使用 reducer
1、先从 React 中导入 useReducer Hook:
import { useReducer } from 'react';
2、替换掉之前的 useState:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks)
useReducer 和 useState 很相似——必须给它传递一个初始状态,它会返回一个有状态的值和一个设置该状态的函数(在这个例子中就是 dispatch 函数)。但是,它们两个之间还是有点差异的。
useReducer 钩子接受 2 个参数:
- 一个 reducer 函数
- 一个初始的 state
它返回如下内容:
- 一个有状态的值
- 一个 dispatch 函数(用来 “派发” 用户操作给 reducer)
对比 useState 和 useReducer ,Reducers 并非没有缺点!以下是比较它们的几种方法:
- 代码体积: 通常,在使用 useState 时,一开始只需要编写少量代码。而 useReducer 必须提前编写 reducer 函数和需要调度的 actions。但是,当多个事件处理程序以相似的方式修改 state 时,useReducer 可以减少代码量。
- 可读性: 当状态更新逻辑足够简单时,useState 的可读性还行。但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。在这种情况下,useReducer 允许你将状态更新逻辑与事件处理程序分离开来。
- 可调试性: 当使用 useState 出现问题时, 你很难发现具体原因以及为什么。 而使用 useReducer 时, 你可以在 reducer 函数中通过打印日志的方式来观察每个状态的更新,以及为什么要更新(来自哪个 action)。 如果所有 action 都没问题,你就知道问题出在了 reducer 本身的逻辑中。 然而,与使用 useState 相比,你必须单步执行更多的代码。
- 可测试性: reducer 是一个不依赖于组件的纯函数。这就意味着你可以单独对它进行测试。一般来说,我们最好是在真实环境中测试组件,但对于复杂的状态更新逻辑,针对特定的初始状态和 action,断言 reducer 返回的特定状态会很有帮助。
- 个人偏好: 并不是所有人都喜欢用 reducer,没关系,这是个人偏好问题。你可以随时在 useState 和 useReducer 之间切换,它们能做的事情是一样的!
编写 reducers 时牢记以下两点:
- reducers 必须是纯粹的。 这一点和 状态更新函数 是相似的,reducers 在是在渲染时运行的!(actions 会排队直到下一次渲染)。 这就意味着 reducers 必须纯净,即当输入相同时,输出也是相同的。它们不应该包含异步请求、定时器或者任何副作用(对组件外部有影响的操作)。它们应该以不可变值的方式去更新 对象 和 数组。
- 每个 action 都描述了一个单一的用户交互,即使它会引发数据的多个变化。 举个例子,如果用户在一个由 reducer 管理的表单(包含五个表单项)中点击了 重置按钮,那么 dispatch 一个 reset_form 的 action 比 dispatch 五个单独的 set_field 的 action 更加合理。如果你在一个 reducer 中打印了所有的 action 日志,那么这个日志应该是很清晰的,它能让你以某种步骤复现已发生的交互或响应。这对代码调试很有帮助!
useReducer 的实现:
function useReducer(reducer, initialState) {const [state, setState] = useState(initialState);// dispatch 实现const dispatch = (action) => {setState((s) => reducer(s, action))}return [state, dispatch];
}
Context
Context 允许父组件向其下层无论多深的任何组件提供信息,而无需通过 props 显式传递。
当需要在组件树中深层传递参数以及需要在组件间复用相同的参数时,传递 props 就会变得很麻烦。最近的根节点父组件可能离需要数据的组件很远,状态提升 到太高的层级会导致 “逐层传递 props” 的情况。React 的 context 功能可以满足在组件树中不需要 props 将数据“直达”到所需的组件中。
可以通过以下三个步骤来使用 context :
1、创建 一个 context。
import { createContext } from 'react';const LevelContext = createContext(1)
createContext 只需默认值这么一个参数。在这里, 1 表示最大的标题级别,但是可以传递任何类型的值(甚至可以传入一个对象)
2、在需要数据的组件内 使用 刚刚创建的 context。
import { useContext } from 'react';function Heading({ children }) {const level = useContext(LevelContext)switch (level) {case 1:return <h1>children</h1>case 2:return <h2>children</h2>case 3:return <h3>children</h3>case 4:return <h4>children</h4>case 5:return <h5>children</h5>case 6:return <h6>children</h6>default:throw Error('未知的 level:' + level);}
}
useContext 是一个 Hook。和 useState 以及 useReducer一样,只能在 React 组件中(不是循环或者条件里)立即调用 Hook。useContext 告诉 React Heading 组件想要读取 LevelContext。
因为上一步 createContext 传入了 1 这个参数,所以 useContext(LevelContext) 会返回 1,把所有的标题都设置为 <h1>
。
3、在指定数据的组件中 提供 这个 context。
function Section({ level, children }) {console.log('children: ', children);return (<LevelContext.Provider value={level}>{children}</LevelContext.Provider>)
}
用 context provider 包裹起来,这告诉 React:“如果在
Context 会穿过中间层级的组件
- 可以在提供 context 的组件和使用它的组件之间的层级插入任意数量的组件。这包括像 这样的内置组件和你自己创建的组件。
- 在 React 中,覆盖来自上层的某些 context 的唯一方法是将子组件包裹到一个提供不同值的 context provider 中。
- 通过 createContext() 创建的每个 context 都和其他 context 完全分离,只有使用和提供 那个特定的 context 的组件才会联系在一起。一个组件可以轻松地使用或者提供许多不同的 context。
在使用 context 之前
使用 Context 看起来非常诱人!然而,这也意味着它也太容易被过度使用了。如果只想把一些 props 传递到多个层级中,这并不意味着你需要把这些信息放到 context 里。在使用 context 之前,可以考虑以下几种替代方案:
- 从 传递 props 开始。 如果组件看起来不起眼,那么通过十几个组件向下传递一堆 props 并不罕见,这样做可以让哪些组件用了哪些数据变得十分清晰!维护你代码的人会很高兴你用 props 让数据流变得更加清晰。
- 抽象组件并 将 JSX 作为 children 传递 给它们。 如果通过很多层不使用该数据的中间组件(并且只会向下传递)来传递数据,这通常意味着在此过程中忘记了抽象组件。举个例子,可能想传递一些像 posts 的数据 props 到不会直接使用这个参数的组件,类似 。取而代之的是,让 Layout 把 children 当做一个参数,然后渲染 。这样就减少了定义数据的组件和使用数据的组件之间的层级。
Context 的使用场景
- 主题: 如果应用允许用户更改其外观(例如暗夜模式),可以在应用顶层放一个 context provider,并在需要调整其外观的组件中使用该 context。
- 当前账户: 许多组件可能需要知道当前登录的用户信息。将它放到 context 中可以方便地在树中的任何位置读取它。某些应用还允许同时操作多个账户(例如,以不同用户的身份发表评论)。在这些情况下,将 UI 的一部分包裹到具有不同账户数据的 provider 中会很方便。
- 路由: 大多数路由解决方案在其内部使用 context 来保存当前路由。这就是每个链接“知道”它是否处于活动状态的方式。如果创建路由库可能也会这么做。
- 状态管理: 随着应用的增长,最终在靠近应用顶部的位置可能会有很多 state。许多遥远的下层组件可能想要修改它们。通常 将 reducer 与 context 搭配使用来管理复杂的状态并将其传递给深层的组件来避免过多的麻烦。
使用 Reducer 和 Context 拓展应用
Reducer 可以整合组件的状态更新逻辑。Context 可以将信息深入传递给其他组件。可以组合使用它们来共同管理一个复杂页面的状态。
Reducer 有助于保持事件处理程序的简短明了。但随着应用规模越来越庞大,就可能会遇到别的困难。如 useReducer 中的示例,目前 tasks 状态和 dispatch 函数仅在顶级 TaskApp 组件中可用。要让其他组件读取任务列表或更改它,必须显式 传递 当前状态和事件处理程序,将其作为 props。如果有成千上百个组件,传递所有状态和函数可能会非常麻烦!
这就是为什么,比起通过 props 传递它们,可能想把 tasks 状态和 dispatch 函数都 放入 context。这样,所有的在 TaskApp 组件树之下的组件都不必一直往下传 props 而可以直接读取 tasks 和 dispatch 函数。下面将介绍如何结合使用 reducer 和 context:
- 创建 context。
- 将 state 和 dispatch 放入 context。
- 在组件树的任何地方 使用 context。
1、创建 context
useReducer 返回当前的 tasks 和 dispatch 函数来更新它们:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks)
为了将它们从组件树往下传,你将 创建 两个不同的 context:
- TasksContext 提供当前的 tasks 列表。
- TasksDispatchContext 提供了一个函数可以让组件分发动作。
const TasksContext = createContext(null)
const TasksDispatchContext = createContext(null)
2、将 state 和 dispatch 函数 放入 context
获取 useReducer() 返回的 tasks 和 dispatch 并将它们 提供 给整个组件树:
const TasksContext = createContext(null)
const TasksDispatchContext = createContext(null)
function TaskAppOne() {const [tasks, dispatch] = useReducer(tasksReducer, initialTasks)return (<TasksContext.Provider value={tasks}><TasksDispatchContext.Provider value={dispatch}>{/* …… */}</TasksDispatchContext.Provider></TasksContext.Provider>)
}
3、在组件树中的任何地方使用 context
现在不需要将 tasks 和事件处理程序在组件树中传递:
<TasksContext.Provider value={tasks}><TasksDispatchContext.Provider value={dispatch}><h1>Day off in Kyoto</h1><AddTask /><TaskList /></TasksDispatchContext.Provider>
</TasksContext.Provider>
任何需要 tasks 的组件都可以从 TaskContext 中读取,任何组件都可以从 context 中读取 dispatch 函数并调用它。TaskAppOne 组件不会向下传递任何事件处理程序,每个组件都会读取它需要的 context。
function TaskList() {const tasks = useContext(TasksContext);// ...
}function AddTask() {const [text, setText] = useState('');const dispatch = useContext(TasksDispatchContext);// ...return (// ...<button onClick={() => {setText('');dispatch({type: 'added',id: nextId++,text: text,});}}>Add</button>// ...
state 仍然 “存在于” 顶层组件中,由 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.Provider value={tasks}><TasksDispatchContext.Provider value={dispatch}>{children}</TasksDispatchContext.Provider></TasksContext.Provider>);
}
这将使 顶层 组件更加直观,也可以从 TasksContext.js 中导出使用 context 的函数:
export function useTasks() {return useContext(TasksContext);
}export function useTasksDispatch() {return useContext(TasksDispatchContext);
}
组件可以通过以下函数读取 context:
const tasks = useTasks();
const dispatch = useTasksDispatch();
像 useTasks 和 useTasksDispatch 这样的函数被称为 自定义 Hook。 如果你的函数名以 use 开头,它就被认为是一个自定义 Hook。这让你可以使用其他 Hook,比如 useContext。