随着应用不断变大,应该更有意识的去关注应用状态如何组织,以及数据如何在组件之间流动。冗余或重复的状态往往是缺陷的根源。
总体步骤如下:
定位组件中不同的视图状态
确定是什么触发了这些 state 的改变
表示内存中的 state(需要使用 useState
)
删除任何不必要的 state 变量
这个 state 是否会导致矛盾?
相同的信息是否已经在另一个 state 变量中存在?
你是否可以通过另一个 state 变量的相反值得到相同的信息?
Reducer 可以合并多个状态变量到一个对象中并巩固所有相关的逻辑!
连接事件处理函数去设置 state
同时展示大量的视图状态,这样的页面通常被称作
living styleguide
或者storybook
挑战:这个表单在两种模式间切换:编辑模式,你可以看到输入框;查看模式,你只能看到结果。按钮的标签会根据你所处的模式在“编辑”和“保存”两者中切换。当你改变输入框的内容时,欢迎信息会最下面实时更新。
import {useState} from 'react';
export default function EditProfile() {
const [display, setDisplay] = useState(false);
const [person, setPerson] = useState({
firstName: 'Joe',
lastName: 'Stan'
});
function handleFirstNameChange(e) {
setPerson({
...person,
[e.target.name]: e.target.value
})
}
function handleLastNameChange(e) {
setPerson({
...person,
[e.target.name]: e.target.value
})
}
return (
<form onSubmit={e => {
e.preventDefault(),
setDisplay(!display)
}}>
<label>
First name:{' '}
{display ?
<input
name="firstName"
value={person.firstName}
onChange={handleFirstNameChange} /> :
<b>{person.firstName}</b>
}
</label>
<label>
Last name:{' '}
{display ?
<input
name="lastName"
value={person.lastName}
onChange={handleLastNameChange} /> :
<b>{person.lastName}</b>
}
</label>
<button type="submit">
{display ? 'Save' : 'Edit'} Profile
</button>
<p><i>Hello, {person.firstName} {person.lastName}!</i></p>
</form>
);
}
updateDOM
函数展示了当你设置 state 时,React 在幕后都做了什么.
// index.js
let firstName = 'Jane';
let lastName = 'Jacobs';
let isEditing = false;
function handleFormSubmit(e) {
e.preventDefault();
setIsEditing(!isEditing);
}
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
function setFirstName(value) {
firstName = value;
updateDOM();
}
function setLastName(value) {
lastName = value;
updateDOM();
}
function setIsEditing(value) {
isEditing = value;
updateDOM();
}
function updateDOM() {
if (isEditing) {
editButton.textContent = 'Save Profile';
hide(firstNameText);
hide(lastNameText);
show(firstNameInput);
show(lastNameInput);
} else {
editButton.textContent = 'Edit Profile';
hide(firstNameInput);
hide(lastNameInput);
show(firstNameText);
show(lastNameText);
}
firstNameText.textContent = firstName;
lastNameText.textContent = lastName;
helloText.textContent = (
'Hello ' +
firstName + ' ' +
lastName + '!'
);
}
function hide(el) {
el.style.display = 'none';
}
function show(el) {
el.style.display = '';
}
let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let firstNameInput = document.getElementById('firstNameInput');
let firstNameText = document.getElementById('firstNameText');
let lastNameInput = document.getElementById('lastNameInput');
let lastNameText = document.getElementById('lastNameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
firstNameInput.oninput = handleFirstNameChange;
lastNameInput.oninput = handleLastNameChange;
/* index.html */
<form id="form">
<label>
First name:
<b id="firstNameText">Janeb>
<input
id="firstNameInput"
value="Jane"
style="display: none">
label>
<label>
Last name:
<b id="lastNameText">Jacobsb>
<input
id="lastNameInput"
value="Jacobs"
style="display: none">
label>
<button type="submit" id="editButton">Edit Profilebutton>
<p><i id="helloText">Hello, Jane Jacobs!i>p>
form>
<style>
* { box-sizing: border-box; }
body { font-family: sans-serif; margin: 20px; padding: 0; }
label { display: block; margin-bottom: 20px; }
style>
合并关联的 state
避免互相矛盾的 state
避免冗余的 state
“镜像”一些 prop 属性会导致混淆,建议使用常量
function Message({ messageColor }) {
const color = messageColor;
只有当想要 忽略特定 props 属性的所有更新时,将 props “镜像”到 state 才有意义。按照惯例,prop 名称以 initial
或 default
开头,以阐明该 prop 的新值将被忽略:
function Message({ initialColor }) {
// 这个 `color` state 变量用于保存 `initialColor` 的 **初始值**。
// 对于 `initialColor` 属性的进一步更改将被忽略。
const [color, setColor] = useState(initialColor);
避免重复的 state
避免深度嵌套的 state
当编写一个组件时,你应该考虑哪些信息应该由父组件控制(通过传递 props
),哪些信息应该由内部state
控制(通过 state
)。
进行状态提升的步骤:
React 使用树形结构来对开发者创造的 UI 进行管理和建模。
根据组件在 UI 树中的位置,React 将它所持有的每个 state 与正确的组件关联起来。 React 在移除一个组件时,也会销毁它的 state。下面是一个组件的添加与删除。
只要一个组件还被渲染在 UI 树的相同位置,React 就会保留它的 state。 如果它被移除,或者一个不同的组件被渲染在相同的位置,那么 React 就会丢掉它的 state。
更新 App
的状态不会重置 Counter
,因为 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>
);
}
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 (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
加一
</button>
</div>
);
}
对 React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置!
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>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
加一
</button>
</div>
);
}
当 Counter
变为 p
时,Counter
会被移除,同时 p
被添加。
当切换回来时,p
会被删除,而 Counter
会被添加。
刚开始 当在相同位置渲染不同的组件时,组件的整个子树都会被重置 当 当切换回来时, 如果想在重新渲染时保留 state,几次渲染中的树形结构就应该相互“匹配”。 方法2:使用 请记住 key 不是全局唯一的。它们只能指定 父组件内部 的顺序。 给 比如聊天应用 Reducer 是处理状态的另一种方式。你可以通过三个步骤将 将设置状态的逻辑 修改 成 比如下面这段代码: 移除所有状态设置逻辑,只留下三个事件处理函数。通过事件处理函数 action 对象可以有多种结构。 按照惯例,通常会添加一个字符串类型的 编写 一个 reducer 函数就是你放置状态逻辑的地方。它接受两个参数,分别为当前 state 和 action 对象,并且返回的是更新后的 state: 在这个例子中,要将状态设置逻辑从事件处理程序移到 reducer 函数中,你需要: 上面语句用了 在组件中 使用 事件处理程序只通过派发 引入 使用 Context 允许父组件向其下层无论多深的任何组件提供信息,而无需通过 props 显式传递。 创建context 使用context 提供context 用 context provider 包裹起来 以提供 Context 让你可以编写“适应周围环境”的组件,并且根据在哪 (或者说 在哪个 context 中)来渲染它们不同的样子。不同的 React context 不会覆盖彼此。Context 会穿过中间的任何组件。 步骤: 为子组件提供 state 和 dispatch 函数: 创建两个 context (一个用于 state,一个用于 dispatch 函数)。 让组件的 context 使用 reducer。 使用组件中需要读取的 context。 像 Counter
。但是当切换成 p
时,React 将 Counter
从 UI 树中移除了并销毁了它的状态。
section
变为 div
时,section
会被删除,新的 div
被添加div
会被删除,新的 section
被添加。
3.4.4 在相同位置重置相同组件的state
key
赋予每个组件一个明确的身份{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
3.4.5 使用
key
重置表单Chat
组件添加一个 key
,就可以保证每次选择一个不同的收件人时,Chat
组件包括其下方树中的state
就会被重新创建。<Chat key={to.id} contact={to} />
3.4.6 为被移除的组件保留
state
3.5 迁移状态逻辑至Reducer中
3.5.1 使用reducer整合状态逻辑
useState
迁移到 useReducer
:
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));
}
dispatch
一个 action
来指明 “用户刚刚做了什么”。(状态更新逻辑则保存在其他地方!),修改后的代码如下:function handleAddTask(text) {
dispatch(
// action 对象
{
type: 'added',
id: nextId++,
text: text,
}
);
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}
type
字段来描述发生了什么,并通过其它字段传递额外的信息。type
是特定于组件的,在这个例子中 added
和 addded_task
都可以。选一个能描述清楚发生的事件的名字!dispatch({
// 针对特定的组件
type: 'what_happened',
// 其它字段放这里
});
reducer
函数;function yourReducer(state, action) {
// 给 React 返回更新后的状态
}
tasks
)作为第一个参数;action
对象作为第二个参数;reducer
返回 下一个 状态(React 会将旧的状态设置为这个最新的状态)。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);
}
}
if/else
语句,但在reducers
中使用switch
语句更加一目了然: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
。action
来指定 发生了什么,而 reducer
函数通过响应 actions
来决定 状态如何更新。3.5.2 对比useState和useReducer
useState
,组件复杂用useReducer
useState
,复杂时useReducer
useReducer
更佳,可以通过打印日志来调试reducer
函数是一个纯函数,可以单独进行测试3.5.3 编写一个好的reducer函数
reducer
必须是一个纯函数action
都描述了一个单一的用户交互,即便它会引发多个数据的变化3.5.4 使用Immer简化reducers
useImmerReducer
:import { useImmerReducer } from 'use-immer';
draft.函数
修改state:function tasksReducer(draft, action) {
switch (action.type) {
case 'added': {
draft.push({
id: action.id,
text: action.text,
done: false,
});
break;
}
case 'changed': {
const index = draft.findIndex((t) => t.id === action.task.id);
draft[index] = action.task;
break;
}
case 'deleted': {
return draft.filter((t) => t.id !== action.id);
}
default: {
throw Error('未知 action:' + action.type);
}
}
}
3.6 使用Context深层传递参数
// LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(1);
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
LevelContext
给它们:import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
3.6.1 context的使用场景
context provider
,并在需要调整其外观的组件中使用该 context。reducer
与 context
搭配使用来管理复杂的状态并将其传递给深层的组件来避免过多的麻烦。3.7 使用Reducer和Context拓展应用
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
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。