react之状态管理

随着应用的不断加大,我们应该更有意识的的去关注我们的应用状态如何去组织,以及数据如何在组件中流动。我们应该去避免冗余或重复的状态,这样可以避免一些缺陷的产生。这时候,如何组织好状态,如何保持状态更新逻辑的可维护性,以及如何跨组件共享状态就变得尤为重要。

使用状态响应输入

在使用React时,我们不需要直接代码层面去修改UI。我们可以直接通过组件的不同状态去展现的UI,然后根据用户输入触发状态修改。

声明式UI与命令式UI的比较

当设计UI交互时,会思考UI如何根据用户的操作而响应变化。想象一个允许用户提交一个答案的表单:

  • 当表单输入的时候,“提交”按钮会变成可用状态
  • 点击“提交”后,表单和提交按钮都会随之变成不可用状态
  • 如果网络请求成功,表单会随之隐藏,同时会出现“提交成功”提示
  • 如果网络请求失败,会出现错误提示信息,表单又变为可用状态

命令式编程中,我们需要根据具体的场景去设计如何实现交互。我们必须根据可能会发生的事情去写一些明确的命令去操作UI。也就是我们需要“命令”每个元素(操作dom),告诉计算机应该如何去更新UI的编程方式被称为命令式编程

对于独立系统来说,命令式控制用户界面的效果也不错,但是如果要实现更为复杂的系统时,代码的组织就会指数级难度增长。

而React就是为了解决这样的问题而诞生。

在React中,不比直接去操作UI(不需要去直接操作dom)。相反,我们只需要声明我们想要显示的内容。React就会通过计算如何去更新UI。

声明式考虑UI

通过上面的思考方式,我们来看看React时如何去实现这个UI。

  1. 定位组件中不同的视图状态
  2. 确定是什么触发这些state的改变
  3. 表示内存中的state(需要使用useState)
  4. 删除任何不必要的state变量
  5. 连接事件处理函数去设置state

步骤1:定位组件中不同的视图状态

在React中,不同的可视化UI界面中用户所有看到的都是不同的“状态”。

  • 无数据:表单有一个不可用状态的“提交”按钮。
  • 输入中:表单有一个可用状态的“提交”按钮。
  • 提交中:表单完全处于不可用状态,加载动画出现。
  • 成功时:显示“成功”的消息而非表单。
  • 错误时:与输入状态类似,但会多错误的消息。

步骤2:确定是什么触发了这些状态的改变

触发state的更新来响应俩种输入:

  • 人为输入。比如点击按钮、在表单中输入内容,或导航到链接。
  • 计算机输入。比如网络请求得到反馈、定时器被触发,或加载一张图片。

以上两种情况中,你必须设置 state 变量 去更新 UI。对于正在开发中的表单来说,你需要改变 state 以响应几个不同的输入:

  • 改变输入框中的文本时(人为)应该根据输入框的内容是否是空值,从而决定将表单的状态从空值状态切换到输入中或切换回原状态。
  • 点击提交按钮时(人为)应该将表单的状态切换到提交中的状态。
  • 网络请求成功后(计算机)应该将表单的状态切换到成功的状态。
  • 网络请求失败后(计算机)应该将表单的状态切换到失败的状态,与此同时,显示错误信息。

步骤3:通过useState表示内存中的state

接下来,我们会需要在内存中通过 useState 表示组件中的视图状态。state的每个部分都是“处在变化中的”,并且需要让“变化大的部分”尽可能的少

  1. 先从绝对必须存在的状态开始。
  2. 接下来,创建一个状态变量去代表想要显示的可时状态
  3. 在很难想出最好的办法时,就从添加足够多的state开始,确保所有可能的视图状态都包含。

最初的想法或许不是最好的,必要时,**重构state**也是步骤中的一部分。

步骤4:删除任何不必要的state变量

我们想要避免state内容中的重复,从而只需要关注必要的部分。这就需要我们花费时间去重构我们的state结构,这样会让我们的组件更容易被理解,从而减少重复避免歧义。主要的目的是防止出现在内存中的state不代表任何我们让用户看到的有效UI的情况。

在创建state变量的时候,我们应该反问自己以下这些问题:

  • 这个state是否会导致矛盾?例如,isTypingisSubmitting 的状态不能同时为 true。矛盾的产生通常说明了这个 state 没有足够的约束条件。两个布尔值有四种可能的组合,但是只有三种对应有效的状态。为了将“不可能”的状态移除,可以将 'typing''submitting' 以及 'success' 这三个中的其中一个与 status 结合。
  • 相同的信息是否已经在另一个 state 变量中存在?另一个矛盾:isEmptyisTyping 不能同时为 true。通过使它们成为独立的 state 变量,可能会导致它们不同步并导致 bug。幸运的是,可以移除 isEmpty 转而用 message.length === 0
  • 是否可以通过另一个 state 变量的相反值得到相同的信息isError 是多余的,因为你可以检查 error !== null

正是因为在不破坏功能的情况下删除其中任何一个状态变量,才可以确定这些都是必要的。

步骤5:连接事件处理函数以设置state

最后,创建事件处理函数去设置state变量。

选择状态结构

良好的状态组织,可以区分易于修改和调试的组件和频繁出问题的组件。最重要的原则是,状态不应该包含冗余或重复的信息。

构建state的原则

当编写一个存有state的组件时,需要我们去选择使用多少个state变量以及他们都是什么数据格式。以下是构建state的原则来指导哦们做出更好的决策:

  1. **合并关联的state**。如果存在同时更新俩个或者多个的state变量,就该考虑将他们合并为一个单独的state变量。
  2. **避免互相矛盾的state**。当state结构存在多个互相矛盾或者“不一致”的state时,就应该避免这个情况。
  3. **避免冗余的state**。如果在渲染期间从组件的props或现有的state变量中计算一些信息,那么这些信息不因该放在该组件的state中。
  4. **避免重复的state**。当统一数据在多个state变量之间或在多个嵌套对象总重复时,会很难保持同步。因此应尽量减少重复。
  5. **避免深层嵌套的state**。深度分层的state更新起来会不方便,最好时构建扁平化的state

这些原则背后的目标是使state易于更新而不引入错误。从state中删除冗余和重复数据有助于所有部分保持同步。这类似于数据库工程师想要 “规范化”数据库结构,以减少出现错误的机会。用爱因斯坦的话说,“让你的状态尽可能简单,但不要过于简单。”

合并关联的state

有时候我们可能不确定使用单个state变量还是多个state变量。

例如下面的存在x、y

是这样做?

1
2
const [x, setX] = useState(0);
const [y, setY] = useState(0);

还是这样做?

1
const [position, setPosition] = useState({ x: 0, y: 0 });

从技术实现上来说,可以使用任何一种方法。但是,如果俩个state变量总是一起变化,则将它们统一成state变量可能会更好。这样我们就不会忘记让它们始终保持同步。

另一种情况是,我们将数据整合到一个对象或一个数组中时,不知道需要多少个state片段。例如,用户可以自定义字段的表单,这样就很有帮助。

注意:如果 state 变量是一个对象时,请记住,不能只更新其中的一个字段 而不显式复制其他字段。例如,在上面的例子中,不能写成 setPosition({ x: 100 }),因为它根本就没有 y 属性! 相反,如果你想要仅设置 x,则可执行 setPosition({ ...position, x: 100 }),或将它们分成两个 state 变量,并执行 setX(100)

避免矛盾的state

看以下例子代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { useState } from 'react';

export default function FeedbackForm() {
const [text, setText] = useState('');
// 俩个状态去控制 很容易去忘记更新
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);
}

if (isSent) {
return <h1>Thanks for feedback!</h1>
}

return (
<form onSubmit={handleSubmit}>
<p>How was your stay at The Prancing Pony?</p>
<textarea
disabled={isSending}
value={text}
onChange={e => setText(e.target.value)}
/>
<br />
<button
disabled={isSending}
type="submit"
>
Send
</button>
{isSending && <p>Sending...</p>}
</form>
);
}

// 假装发送一条消息。
function sendMessage(text) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}

观察这段代码,其实是有效的,但是会存在一些state“极难处理”。如果忘记 setIsSentsetIsSending,则可能会出现 isSendingisSent 同时为 true 的情况。组件越复杂,就很难理解发生了什么。

因为 isSendingisSent 不应同时为 true,所以最好用一个 status 变量来代替它们,这个 state 变量可以采取三种有效状态其中之一'typing' (初始), 'sending', 和 'sent':

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { useState } from 'react';

export default function FeedbackForm() {
const [text, setText] = useState('');
// 将state合并为三个状态 动态修改去做条件判断
const [status, setStatus] = useState('typing');

async function handleSubmit(e) {
e.preventDefault();
setStatus('sending');
await sendMessage(text);
setStatus('sent');
}

// 它们不是 state 变量,所以不必担心它们彼此失去同步。
const isSending = status === 'sending';
const isSent = status === 'sent';

if (isSent) {
return <h1>Thanks for feedback!</h1>
}

return (
<form onSubmit={handleSubmit}>
<p>How was your stay at The Prancing Pony?</p>
<textarea
disabled={isSending}
value={text}
onChange={e => setText(e.target.value)}
/>
<br />
<button
disabled={isSending}
type="submit"
>
Send
</button>
{isSending && <p>Sending...</p>}
</form>
);
}

// 假装发送一条消息。
function sendMessage(text) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}

避免冗余的state

如果我们在渲染期间从组件的props或现有的state变量中计算出一些信息,则不应该把这些信息放到该组件的state中。

仔细看看以下代码。他可以允许,仔细观察就能发现其中的冗余之处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { useState } from 'react';

export default function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// 没错就是这里 这里fullName其实可以通过firstName 和 lastName计算得出
// const [fullName, setFullName] = useState('');
// 就可以替换成以下这段代码
// 这样就不需要每次在依赖的state中去重新修改状态
const fullName = firstName + ''+lastName

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 (
<>
<h2>Let’s check you in</h2>
<label>
First name:{' '}
<input
value={firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:{' '}
<input
value={lastName}
onChange={handleLastNameChange}
/>
</label>
<p>
Your ticket will be issued to: <b>{fullName}</b>
</p>
</>
);
}


为什么能用常量去做呢?这是因为事件处理程序不需要任何操作去更新它,当我们调用setFirstNamesetLastName 时,你会触发一次重新渲染,然后下一个 fullName 将从新数据中计算出来。

深入探讨:不要在 state 中镜像 props

看看以下代码:

1
2
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);

这里,一个 color state 变量被初始化为 messageColor 的 prop 值。这段代码的问题在于,如果父组件稍后传递不同的 messageColor 值(例如,将其从 'blue' 更改为 'red'),则 color state 变量将不会更新! state 仅在第一次渲染期间初始化。

这就是为什么在 state 变量中,“镜像”一些 prop 属性会导致混淆的原因。相反,你要在代码中直接使用 messageColor 属性。如果想给它起一个更短的名称,请使用常量:

1
2
3
function Message({ messageColor }) {

const color = messageColor;

这种写法就不会与从父组件传递的属性失去同步。

只有当你 想要 忽略特定 props 属性的所有更新时,将 props “镜像”到 state 才有意义。按照惯例,prop 名称以 initialdefault 开头,以阐明该 prop 的新值将被忽略:

1
2
3
4
function Message({ initialColor }) {
// 这个 `color` state 变量用于保存 `initialColor` 的 **初始值**。
// 对于 `initialColor` 属性的进一步更改将被忽略。
const [color, setColor] = useState(initialColor);

避免重复的state

下面这是一个可以选择的菜单列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 (
<>
<h2>What's your travel snack?</h2>
<ul>
{items.map(item => (
<li key={item.id}>
{item.title}
{' '}
<button onClick={() => {
setSelectedItem(item);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.title}.</p>
</>
);
}

这里的所选元素作为对象存储在selectedItem state变量中。然而,**selecteItem的内容与items列表中的某一项是同一个对象**,这意味着关于该项本身的信息在俩个地方产生了重复。

我们在将我们的组件进行修改,让每个项目都可以编辑,看看会出现什么问题?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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]
);

function handleItemChange(id, e) {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
title: e.target.value,
};
} else {
return item;
}
}));
}

return (
<>
<h2>What's your travel snack?</h2>
<ul>
{items.map((item, index) => (
<li key={item.id}>
<input
value={item.title}
onChange={e => {
handleItemChange(item.id, e)
}}
/>
{' '}
<button onClick={() => {
setSelectedItem(item);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.title}.</p>
</>
);
}

注意,当我们选择“choose”按钮时,然后编辑它,输入会更新但是底部的标签不会反应编辑的内容。这是因为我们存在了重复的state,并且忘记去更新了selectedItem

我们也可以选择去更新selectedItem,但更简单的办法是去消除重复项。在下面对代码中,我们就可以将selectedId保存在state中,而不是在selectedItem对象中(它创建了一个与items内重复的对象),然后通过items数组去过滤出具有该ID的项,以此来获取selectedItem:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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 (
<>
<h2>What's your travel snack?</h2>
<ul>
{items.map((item, index) => (
<li key={item.id}>
<input
value={item.title}
onChange={e => {
handleItemChange(item.id, e)
}}
/>
{' '}
<button onClick={() => {
setSelectedId(item.id);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.title}.</p>
</>
);
}

在或者,我们可以将所选择的索引保存在state中。

state 过去常常是这样赋值的:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

改了之后是这样的:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedId = 0

这样,就不存在重复的state,只保留了必要的state。这样的话,当我们编辑selected元素,下面的标签就会立即更新。这是因为setItem会触发重新渲染,而item.find(...)会找到带有更新文本的元素,我们不需要在state中保存选定的元素,因为只有选定的ID是必要的。其余的都可以在渲染期间计算得出。

避免深度的嵌套state

想象一下,一个由行星、大陆和国家组成的旅行计划。你可能会尝试使用嵌套对象和数组来构建它的 state,就像下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
// app.js
import { useState } from 'react';
import { initialTravelPlan } from './places.js';

// 递归展示
function PlaceTree({ place }) {
const childPlaces = place.childPlaces;
return (
<li>
{place.title}
{childPlaces.length > 0 && (
<ol>
{childPlaces.map(place => (
<PlaceTree key={place.id} place={place} />
))}
</ol>
)}
</li>
);
}

export default function TravelPlan() {
const [plan, setPlan] = useState(initialTravelPlan);
const planets = plan.childPlaces;
return (
<>
<h2>Places to visit</h2>
<ol>
{planets.map(place => (
<PlaceTree key={place.id} place={place} />
))}
</ol>
</>
);
}
// place.js
export const initialTravelPlan = {
id: 0,
title: '(Root)',
childPlaces: [{
id: 1,
title: 'Earth',
childPlaces: [{
id: 2,
title: 'Africa',
childPlaces: [{
id: 3,
title: 'Botswana',
childPlaces: []
}, {
id: 4,
title: 'Egypt',
childPlaces: []
}, {
id: 5,
title: 'Kenya',
childPlaces: []
}, {
id: 6,
title: 'Madagascar',
childPlaces: []
}, {
id: 7,
title: 'Morocco',
childPlaces: []
}, {
id: 8,
title: 'Nigeria',
childPlaces: []
}, {
id: 9,
title: 'South Africa',
childPlaces: []
}]
}, {
id: 10,
title: 'Americas',
childPlaces: [{
id: 11,
title: 'Argentina',
childPlaces: []
}, {
id: 12,
title: 'Brazil',
childPlaces: []
}, {
id: 13,
title: 'Barbados',
childPlaces: []
}, {
id: 14,
title: 'Canada',
childPlaces: []
}, {
id: 15,
title: 'Jamaica',
childPlaces: []
}, {
id: 16,
title: 'Mexico',
childPlaces: []
}, {
id: 17,
title: 'Trinidad and Tobago',
childPlaces: []
}, {
id: 18,
title: 'Venezuela',
childPlaces: []
}]
}, {
id: 19,
title: 'Asia',
childPlaces: [{
id: 20,
title: 'China',
childPlaces: []
}, {
id: 21,
title: 'India',
childPlaces: []
}, {
id: 22,
title: 'Singapore',
childPlaces: []
}, {
id: 23,
title: 'South Korea',
childPlaces: []
}, {
id: 24,
title: 'Thailand',
childPlaces: []
}, {
id: 25,
title: 'Vietnam',
childPlaces: []
}]
}, {
id: 26,
title: 'Europe',
childPlaces: [{
id: 27,
title: 'Croatia',
childPlaces: [],
}, {
id: 28,
title: 'France',
childPlaces: [],
}, {
id: 29,
title: 'Germany',
childPlaces: [],
}, {
id: 30,
title: 'Italy',
childPlaces: [],
}, {
id: 31,
title: 'Portugal',
childPlaces: [],
}, {
id: 32,
title: 'Spain',
childPlaces: [],
}, {
id: 33,
title: 'Turkey',
childPlaces: [],
}]
}, {
id: 34,
title: 'Oceania',
childPlaces: [{
id: 35,
title: 'Australia',
childPlaces: [],
}, {
id: 36,
title: 'Bora Bora (French Polynesia)',
childPlaces: [],
}, {
id: 37,
title: 'Easter Island (Chile)',
childPlaces: [],
}, {
id: 38,
title: 'Fiji',
childPlaces: [],
}, {
id: 39,
title: 'Hawaii (the USA)',
childPlaces: [],
}, {
id: 40,
title: 'New Zealand',
childPlaces: [],
}, {
id: 41,
title: 'Vanuatu',
childPlaces: [],
}]
}]
}, {
id: 42,
title: 'Moon',
childPlaces: [{
id: 43,
title: 'Rheita',
childPlaces: []
}, {
id: 44,
title: 'Piccolomini',
childPlaces: []
}, {
id: 45,
title: 'Tycho',
childPlaces: []
}]
}, {
id: 46,
title: 'Mars',
childPlaces: [{
id: 47,
title: 'Corn Town',
childPlaces: []
}, {
id: 48,
title: 'Green Hill',
childPlaces: []
}]
}]
};

这时候如果我们想添加一个按钮来删除已经去过的地方。该如何做呢?更新嵌套的 state 需要从更改部分一直向上复制对象。删除一个深度嵌套的地点将涉及复制其整个父级地点链。这样的代码可能非常冗长。

如果 state 嵌套太深,难以轻松更新,可以考虑将其“扁平化”。 这里可以通过一个方法来重构上面这个数据:不同树状结构,每个节点的place都是一个包含其子节点的数组,我们可以让每个节点的place作为数组保存其子节点的ID。然后存储一个节点ID与相应节点的映射关系。

看看下面重组的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
// app.js
import { useState } from 'react';
import { initialTravelPlan } from './places.js';

// 先找到该节点的 孩子id 再去递归id去寻找
function PlaceTree({ id, placesById }) {
const place = placesById[id];
const childIds = place.childIds;
return (
<li>
{place.title}
{childIds.length > 0 && (
<ol>
{childIds.map(childId => (
<PlaceTree
key={childId}
id={childId}
placesById={placesById}
/>
))}
</ol>
)}
</li>
);
}

export default function TravelPlan() {
const [plan, setPlan] = useState(initialTravelPlan);
const root = plan[0];
const planetIds = root.childIds;
return (
<>
<h2>Places to visit</h2>
<ol>
{planetIds.map(id => (
<PlaceTree
key={id}
id={id}
placesById={plan}
/>
))}
</ol>
</>
);
}
// places.js
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: []
},
4: {
id: 4,
title: 'Egypt',
childIds: []
},
5: {
id: 5,
title: 'Kenya',
childIds: []
},
6: {
id: 6,
title: 'Madagascar',
childIds: []
},
7: {
id: 7,
title: 'Morocco',
childIds: []
},
8: {
id: 8,
title: 'Nigeria',
childIds: []
},
9: {
id: 9,
title: 'South Africa',
childIds: []
},
10: {
id: 10,
title: 'Americas',
childIds: [11, 12, 13, 14, 15, 16, 17, 18],
},
11: {
id: 11,
title: 'Argentina',
childIds: []
},
12: {
id: 12,
title: 'Brazil',
childIds: []
},
13: {
id: 13,
title: 'Barbados',
childIds: []
},
14: {
id: 14,
title: 'Canada',
childIds: []
},
15: {
id: 15,
title: 'Jamaica',
childIds: []
},
16: {
id: 16,
title: 'Mexico',
childIds: []
},
17: {
id: 17,
title: 'Trinidad and Tobago',
childIds: []
},
18: {
id: 18,
title: 'Venezuela',
childIds: []
},
19: {
id: 19,
title: 'Asia',
childIds: [20, 21, 22, 23, 24, 25],
},
20: {
id: 20,
title: 'China',
childIds: []
},
21: {
id: 21,
title: 'India',
childIds: []
},
22: {
id: 22,
title: 'Singapore',
childIds: []
},
23: {
id: 23,
title: 'South Korea',
childIds: []
},
24: {
id: 24,
title: 'Thailand',
childIds: []
},
25: {
id: 25,
title: 'Vietnam',
childIds: []
},
26: {
id: 26,
title: 'Europe',
childIds: [27, 28, 29, 30, 31, 32, 33],
},
27: {
id: 27,
title: 'Croatia',
childIds: []
},
28: {
id: 28,
title: 'France',
childIds: []
},
29: {
id: 29,
title: 'Germany',
childIds: []
},
30: {
id: 30,
title: 'Italy',
childIds: []
},
31: {
id: 31,
title: 'Portugal',
childIds: []
},
32: {
id: 32,
title: 'Spain',
childIds: []
},
33: {
id: 33,
title: 'Turkey',
childIds: []
},
34: {
id: 34,
title: 'Oceania',
childIds: [35, 36, 37, 38, 39, 40, 41],
},
35: {
id: 35,
title: 'Australia',
childIds: []
},
36: {
id: 36,
title: 'Bora Bora (French Polynesia)',
childIds: []
},
37: {
id: 37,
title: 'Easter Island (Chile)',
childIds: []
},
38: {
id: 38,
title: 'Fiji',
childIds: []
},
39: {
id: 40,
title: 'Hawaii (the USA)',
childIds: []
},
40: {
id: 40,
title: 'New Zealand',
childIds: []
},
41: {
id: 41,
title: 'Vanuatu',
childIds: []
},
42: {
id: 42,
title: 'Moon',
childIds: [43, 44, 45]
},
43: {
id: 43,
title: 'Rheita',
childIds: []
},
44: {
id: 44,
title: 'Piccolomini',
childIds: []
},
45: {
id: 45,
title: 'Tycho',
childIds: []
},
46: {
id: 46,
title: 'Mars',
childIds: [47, 48]
},
47: {
id: 47,
title: 'Corn Town',
childIds: []
},
48: {
id: 48,
title: 'Green Hill',
childIds: []
}
};

此刻,state 已经“扁平化”(也称为“规范化”),更新嵌套项会变得更加容易。

现在删除一个地点的话,只需要更新两个 state 级别:

  • 父级 地点的更新版本应该从其 childIds 数组中排除已删除的 ID。
  • 其根级“表”对象的更新版本应包括父级地点的更新版本。

看以下代码处理的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
// app.js
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 (
<>
<h2>Places to visit</h2>
<ol>
{planetIds.map(id => (
<PlaceTree
key={id}
id={id}
parentId={0}
placesById={plan}
onComplete={handleComplete}
/>
))}
</ol>
</>
);
}

function PlaceTree({ id, parentId, placesById, onComplete }) {
const place = placesById[id];
const childIds = place.childIds;
return (
<li>
{place.title}
<button onClick={() => {
onComplete(parentId, id);
}}>
Complete
</button>
{childIds.length > 0 &&
<ol>
{childIds.map(childId => (
<PlaceTree
key={childId}
id={childId}
parentId={id}
placesById={placesById}
onComplete={onComplete}
/>
))}
</ol>
}
</li>
);
}
// place.js
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: []
},
4: {
id: 4,
title: 'Egypt',
childIds: []
},
5: {
id: 5,
title: 'Kenya',
childIds: []
},
6: {
id: 6,
title: 'Madagascar',
childIds: []
},
7: {
id: 7,
title: 'Morocco',
childIds: []
},
8: {
id: 8,
title: 'Nigeria',
childIds: []
},
9: {
id: 9,
title: 'South Africa',
childIds: []
},
10: {
id: 10,
title: 'Americas',
childIds: [11, 12, 13, 14, 15, 16, 17, 18],
},
11: {
id: 11,
title: 'Argentina',
childIds: []
},
12: {
id: 12,
title: 'Brazil',
childIds: []
},
13: {
id: 13,
title: 'Barbados',
childIds: []
},
14: {
id: 14,
title: 'Canada',
childIds: []
},
15: {
id: 15,
title: 'Jamaica',
childIds: []
},
16: {
id: 16,
title: 'Mexico',
childIds: []
},
17: {
id: 17,
title: 'Trinidad and Tobago',
childIds: []
},
18: {
id: 18,
title: 'Venezuela',
childIds: []
},
19: {
id: 19,
title: 'Asia',
childIds: [20, 21, 22, 23, 24, 25],
},
20: {
id: 20,
title: 'China',
childIds: []
},
21: {
id: 21,
title: 'India',
childIds: []
},
22: {
id: 22,
title: 'Singapore',
childIds: []
},
23: {
id: 23,
title: 'South Korea',
childIds: []
},
24: {
id: 24,
title: 'Thailand',
childIds: []
},
25: {
id: 25,
title: 'Vietnam',
childIds: []
},
26: {
id: 26,
title: 'Europe',
childIds: [27, 28, 29, 30, 31, 32, 33],
},
27: {
id: 27,
title: 'Croatia',
childIds: []
},
28: {
id: 28,
title: 'France',
childIds: []
},
29: {
id: 29,
title: 'Germany',
childIds: []
},
30: {
id: 30,
title: 'Italy',
childIds: []
},
31: {
id: 31,
title: 'Portugal',
childIds: []
},
32: {
id: 32,
title: 'Spain',
childIds: []
},
33: {
id: 33,
title: 'Turkey',
childIds: []
},
34: {
id: 34,
title: 'Oceania',
childIds: [35, 36, 37, 38, 39, 40, 41],
},
35: {
id: 35,
title: 'Australia',
childIds: []
},
36: {
id: 36,
title: 'Bora Bora (French Polynesia)',
childIds: []
},
37: {
id: 37,
title: 'Easter Island (Chile)',
childIds: []
},
38: {
id: 38,
title: 'Fiji',
childIds: []
},
39: {
id: 39,
title: 'Hawaii (the USA)',
childIds: []
},
40: {
id: 40,
title: 'New Zealand',
childIds: []
},
41: {
id: 41,
title: 'Vanuatu',
childIds: []
},
42: {
id: 42,
title: 'Moon',
childIds: [43, 44, 45]
},
43: {
id: 43,
title: 'Rheita',
childIds: []
},
44: {
id: 44,
title: 'Piccolomini',
childIds: []
},
45: {
id: 45,
title: 'Tycho',
childIds: []
},
46: {
id: 46,
title: 'Mars',
childIds: [47, 48]
},
47: {
id: 47,
title: 'Corn Town',
childIds: []
},
48: {
id: 48,
title: 'Green Hill',
childIds: []
}
};

这样就可以随心所欲嵌套state,“扁平化”可以解决很多问题。这使得state更容易更新,确保嵌套对象的不同部分中没有重复。

在组件间共享状态

如果我们需要俩个组件的状态始终同步修改。我们可以将相关状态从这俩个组件上移除,将这些状态移到最近的父级组件,然后通过props将这些状态传递给这俩个组件。这被称为“状态提升”。

对state进行保留和重置状态

当我们重新渲染一个组件时,React需要决定组件树哪些部分要保留和更新,以及丢弃或重新创建。在大多数情况下,React的自动处理机制已经做了大部分工作。默认情况下,React会保留树中与先前渲染组件树“匹配”的部分。

React允许覆盖默认行为,这时候可以通过向组件传递一个唯一key强制重置其状态。这将告诉React,组件需要重新渲染。

提取状态到reducer中

对于需要更新多个状态的组件来说,会存在过于分散的事件处理程序。对于这种情况,我们可以在组件外部将所有的状态更新逻辑合并到一个称为“reducer”的函数中。这样,事件处理程序就会变的简洁,只需要我们指定对应的“action”。同时定义,reducer函数指定状态因该如何更新去响应每个action!

使用Context进行深层数据传递

通常,我们会存在需要通过props将信息从父组件传递给子组件。如果需要在组件树中深入传递prop,或者树中许多组件都需要使用相同的prop,那么传递prop可能会变得麻烦。Context允许父组件将一些信息提供给它下层的任何组件,不管组件多深层也无需通过props逐层透传。

使用Reducer和Context进行状态扩展

Reducer可以帮助我们合并组件的状态更新逻辑。Context可以帮助我们将信息深处传递给其他组件。可以将二者结合使用,以管理复杂应用的状态。