选择状态结构
良好地构建状态可以区分一个易于修改和调试的组件,以及一个经常产生错误的组件。 以下是构建状态时应考虑的一些提示。
你将学习
- 何时使用单个与多个状态变量
- 组织状态时要避免什么
- 如何解决状态结构的常见问题
结构化状态的原则
当您编写一个包含某些状态的组件时,您必须选择使用多少个状态变量以及它们的数据应该是什么形状。 虽然即使使用次优状态结构也可以编写正确的程序,但有一些原则可以指导您做出更好的选择:
- 组相关状态。 如果您总是同时更新两个或多个状态变量,请考虑将它们合并为一个状态变量。
- 避免状态上的矛盾。 当状态的结构方式使多个状态可能相互矛盾和“不一致”时,就会为错误留下空间。 尽量避免这种情况。
- 避免冗余状态。 如果您可以在渲染期间从组件的 props 或其现有状态变量中计算出一些信息,则不应将该信息放入该组件的状态中。
- 避免状态重复。 当相同的数据在多个状态变量之间或嵌套对象中重复时,很难使它们保持同步。 尽可能减少重复。
- 避免深度嵌套状态。 层次很深的状态更新起来不是很方便。 如果可能,更喜欢以扁平的方式构建状态。
这些原则背后的目标是使状态易于更新而不会引入错误。 从状态中删除冗余和重复数据有助于确保其所有部分保持同步。 这类似于数据库工程师可能希望“规范化”数据库结构以减少出现错误的可能性。 套用阿尔伯特·爱因斯坦的话,“让你的状态尽可能简单——但不要更简单。”
现在让我们看看这些原则是如何应用到行动中的。
分组相关状态
有时您可能不确定是使用单个还是多个状态变量。
你应该这样做吗?
const [x, setX] = useState(0);
const [y, setY] = useState(0);
或者这样做
const [position, setPosition] = useState({ x: 0, y: 0 });
从技术上讲,您可以使用这些方法中的任何一种。 但是,如果某些两个状态变量总是一起变化,那么将它们统一为一个状态变量可能是个好主意。 这样你不会忘记始终保持它们同步,就像在这个例子中移动光标更新红点的两个坐标:
import { useState } from 'react';
export default function MovingDot() {
const [position, setPosition] = useState({
x: 0,
y: 0
});
return (
<div
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}>
<div style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}} />
</div>
)
}
将数据分组到一个对象或数组中的另一种情况是,当您不知道需要多少个不同的状态时。 例如,当您有一个用户可以添加自定义字段的表单时,它会很有帮助。
"陷阱
如果您的状态变量是一个对象,请记住您不能只更新其中的一个字段而不显式复制其他字段。 例如,您不能在上面的示例中执行 setPosition({ x: 100 }),因为它根本没有 y 属性! 相反,如果您想单独设置 x,您可以执行 setPosition({ ...position, x: 100 }),或者将它们分成两个状态变量并执行 setX(100)。
避免状态矛盾
这是带有 isSending 和 isSent 状态变量的酒店反馈表:
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>
);
}
// Pretend to send a message.
function sendMessage(text) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}
虽然此代码有效,但它为“不可能”状态敞开了大门。 例如,如果您忘记同时调用 setIsSent 和 setIsSending,您可能会遇到 isSending 和 isSent 同时为真的情况。 您的组件越复杂,就越难理解发生了什么。
由于 isSending 和 isSent 绝不能同时为真,因此最好将它们替换为一个可能采用三种有效状态之一的状态status变量:'typing'(初始)、'sending' 和 'sent':
import { useState } from 'react';
export default function FeedbackForm() {
const [text, setText] = useState('');
const [status, setStatus] = useState('typing');
async function handleSubmit(e) {
e.preventDefault();
setStatus('sending');
await sendMessage(text);
setStatus('sent');
}
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>
);
}
// Pretend to send a message.
function sendMessage(text) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}
您仍然可以声明一些常量以提高可读性:
const isSending = status === 'sending';
const isSent = status === 'sent';
但它们不是状态变量,因此您无需担心它们彼此不同步。
避免状态冗余
如果您可以在渲染期间从组件的 props 或其现有状态变量中计算出一些信息,则不应将该信息放入该组件的状态中。
例如,采用这种形式。 它有效,但你能在其中找到任何冗余状态吗?
import { useState } from 'react';
export default function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
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>
</>
);
}
此表单具有三个状态变量:firstName、lastName 和 fullName。 但是,fullName 是多余的。 您始终可以在渲染期间从 firstName 和 lastName 计算 fullName,因此将其从状态中删除。
你可以这样做:
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = firstName + ' ' + lastName;
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
在这里,fullName 不是状态变量。 相反,它是在渲染期间计算的:
因此,更改处理程序不需要做任何特殊的事情来更新它。 当您调用 setFirstName 或 setLastName 时,您会触发重新渲染,然后下一个 fullName 将从新数据中计算出来。
深度阅读:不要再将props镜像保存为state
冗余状态的一个常见示例是这样的代码:
function Message({ messageColor }) { const [color, setColor] = useState(messageColor);
在这里,颜色状态变量被初始化为 messageColor 属性。 问题在于,如果父组件稍后传递了不同的 messageColor 值(例如,'red' 而不是 'blue'),颜色状态变量将不会更新! 状态仅在第一次渲染期间初始化。
这就是为什么在状态变量中“镜像”某些 prop 会导致混淆。 相反,直接在您的代码中使用 messageColor 道具。 如果你想给它一个更短的名字,使用一个常量:
function Message({ messageColor }) { const color = messageColor;
这样它就不会与从父组件传递的 prop 不同步。
仅当您想忽略特定道具的所有更新时,将prop“镜像”到状态中才有意义。 按照惯例,prop 名称以 initial 或 default 开头,以阐明其新值将被忽略:
function Message({ initialColor }) { // The `color` state variable holds the *first* value of `initialColor`. // Further changes to the `initialColor` prop are ignored. const [color, setColor] = useState(initialColor);
避免状态重复
此菜单列表组件可让您从多种旅行小吃中选择一种:
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 状态变量中。 然而,这并不好:selectedItem 的内容与项目列表中的项目之一是同一个对象。 这意味着有关项目本身的信息在两个地方重复。
为什么这是个问题? 让我们让每个项目都可编辑:
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>
</>
);
}
请注意,如果您先在项目上单击“选择”然后对其进行编辑,输入会更新,但底部的标签不会反映编辑内容。 这是因为您有重复的状态,而您忘记更新 selectedItem。
尽管您也可以更新 selectedItem,但更简单的解决方法是删除重复项。 在此示例中,您将 selectedId 保持在 state 中,而不是 selectedItem 对象(它在项目内部创建对象的重复),然后通过在项目数组中搜索具有该 ID 的项目来获取 selectedItem:
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>
</>
);
}
(或者,您可以保持所选索引的状态。)
状态曾经像这样被复制:
- items = [{ id: 0, title: '椒盐卷饼'}, ...]
- selectedItem = {id: 0, title: '椒盐卷饼'}
但是改完之后是这样的:
- items = [{ id: 0, title: '椒盐卷饼'}, ...]
- 选择的 ID = 0
重复没有了,你只保留本质状态!
现在,如果您编辑所选项目,下面的消息将立即更新。 这是因为 setItems 会触发重新渲染,而 items.find(...) 会找到具有更新标题的项目。 您不需要将所选项目保持在状态中,因为只有所选 ID 是必需的。 其余的可以在渲染期间计算。
避免深度嵌套状态
想象一个由行星、大陆和国家组成的旅行计划。 您可能会想使用嵌套对象和数组来构建其状态,如本例所示:
places.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: 'Hong Kong',
childPlaces: []
}, {
id: 22,
title: 'India',
childPlaces: []
}, {
id: 23,
title: 'Singapore',
childPlaces: []
}, {
id: 24,
title: 'South Korea',
childPlaces: []
}, {
id: 25,
title: 'Thailand',
childPlaces: []
}, {
id: 26,
title: 'Vietnam',
childPlaces: []
}]
}, {
id: 27,
title: 'Europe',
childPlaces: [{
id: 28,
title: 'Croatia',
childPlaces: [],
}, {
id: 29,
title: 'France',
childPlaces: [],
}, {
id: 30,
title: 'Germany',
childPlaces: [],
}, {
id: 31,
title: 'Italy',
childPlaces: [],
}, {
id: 32,
title: 'Portugal',
childPlaces: [],
}, {
id: 33,
title: 'Spain',
childPlaces: [],
}, {
id: 34,
title: 'Turkey',
childPlaces: [],
}]
}, {
id: 35,
title: 'Oceania',
childPlaces: [{
id: 36,
title: 'Australia',
childPlaces: [],
}, {
id: 37,
title: 'Bora Bora (French Polynesia)',
childPlaces: [],
}, {
id: 38,
title: 'Easter Island (Chile)',
childPlaces: [],
}, {
id: 39,
title: 'Fiji',
childPlaces: [],
}, {
id: 40,
title: 'Hawaii (the USA)',
childPlaces: [],
}, {
id: 41,
title: 'New Zealand',
childPlaces: [],
}, {
id: 42,
title: 'Vanuatu',
childPlaces: [],
}]
}]
}, {
id: 43,
title: 'Moon',
childPlaces: [{
id: 44,
title: 'Rheita',
childPlaces: []
}, {
id: 45,
title: 'Piccolomini',
childPlaces: []
}, {
id: 46,
title: 'Tycho',
childPlaces: []
}]
}, {
id: 47,
title: 'Mars',
childPlaces: [{
id: 48,
title: 'Corn Town',
childPlaces: []
}, {
id: 49,
title: 'Green Hill',
childPlaces: []
}]
}]
};
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>
</>
);
}
现在假设你想添加一个按钮来删除你已经访问过的地方。 你会怎么做? 更新嵌套状态涉及从更改的部分一直向上复制对象。 删除深度嵌套的位置将涉及复制其整个父位置链。 这样的代码可能非常冗长。
如果状态嵌套太多而不易更新,请考虑使其“扁平化”。 这是您可以重组此数据的一种方法。 您可以让每个地点保存其子地点 ID 的数组,而不是每个地点都有其子地点数组的树状结构。 然后你可以存储一个从每个地点ID到对应地点的映射。
这种数据重组可能会让您想起看到的数据库表:
import { useState } from 'react';
import { initialTravelPlan } from './places.js';
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>
</>
);
}
现在状态是“平坦的”(也称为“规范化”),更新嵌套项变得更容易。
为了现在删除一个地方,你只需要更新两个级别的状态:
- 其父位置的更新版本应从其 childIds 数组中排除已删除的 ID。
- 根“表”对象的更新版本应包括父位置的更新版本。
下面是一个如何去做的例子:
places.js
export const initialTravelPlan = {
0: {
id: 0,
title: '(Root)',
childIds: [1, 43, 47],
},
1: {
id: 1,
title: 'Earth',
childIds: [2, 10, 19, 27, 35]
},
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, 26],
},
20: {
id: 20,
title: 'China',
childIds: []
},
21: {
id: 21,
title: 'Hong Kong',
childIds: []
},
22: {
id: 22,
title: 'India',
childIds: []
},
23: {
id: 23,
title: 'Singapore',
childIds: []
},
24: {
id: 24,
title: 'South Korea',
childIds: []
},
25: {
id: 25,
title: 'Thailand',
childIds: []
},
26: {
id: 26,
title: 'Vietnam',
childIds: []
},
27: {
id: 27,
title: 'Europe',
childIds: [28, 29, 30, 31, 32, 33, 34],
},
28: {
id: 28,
title: 'Croatia',
childIds: []
},
29: {
id: 29,
title: 'France',
childIds: []
},
30: {
id: 30,
title: 'Germany',
childIds: []
},
31: {
id: 31,
title: 'Italy',
childIds: []
},
32: {
id: 32,
title: 'Portugal',
childIds: []
},
33: {
id: 33,
title: 'Spain',
childIds: []
},
34: {
id: 34,
title: 'Turkey',
childIds: []
},
35: {
id: 35,
title: 'Oceania',
childIds: [36, 37, 38, 39, 40, 41,, 42],
},
36: {
id: 36,
title: 'Australia',
childIds: []
},
37: {
id: 37,
title: 'Bora Bora (French Polynesia)',
childIds: []
},
38: {
id: 38,
title: 'Easter Island (Chile)',
childIds: []
},
39: {
id: 39,
title: 'Fiji',
childIds: []
},
40: {
id: 40,
title: 'Hawaii (the USA)',
childIds: []
},
41: {
id: 41,
title: 'New Zealand',
childIds: []
},
42: {
id: 42,
title: 'Vanuatu',
childIds: []
},
43: {
id: 43,
title: 'Moon',
childIds: [44, 45, 46]
},
44: {
id: 44,
title: 'Rheita',
childIds: []
},
45: {
id: 45,
title: 'Piccolomini',
childIds: []
},
46: {
id: 46,
title: 'Tycho',
childIds: []
},
47: {
id: 47,
title: 'Mars',
childIds: [48, 49]
},
48: {
id: 48,
title: 'Corn Town',
childIds: []
},
49: {
id: 49,
title: 'Green Hill',
childIds: []
}
};
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];
// Create a new version of the parent place
// that doesn't include this child ID.
const nextParent = {
...parent,
childIds: parent.childIds
.filter(id => id !== childId)
};
// Update the root state object...
setPlan({
...plan,
// ...so that it has the updated parent.
[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>
);
}
您可以随心所欲地嵌套状态,但使其“扁平化”可以解决许多问题。 它使状态更容易更新,并有助于确保您不会在嵌套对象的不同部分出现重复。
深度阅读:提升内存使用率
理想情况下,您还可以从“表”对象中删除已删除的项目(及其子项!)以提高内存使用率。 这个版本就是这样做的。 它还使用 Immer 使更新逻辑更加简洁。
import { useImmer } from 'use-immer';
import { initialTravelPlan } from './places.js';
export default function TravelPlan() {
const [plan, updatePlan] = useImmer(initialTravelPlan);
function handleComplete(parentId, childId) {
updatePlan(draft => {
// Remove from the parent place's child IDs.
const parent = draft[parentId];
parent.childIds = parent.childIds
.filter(id => id !== childId);
// Forget this place and all its subtree.
deleteAllChildren(childId);
function deleteAllChildren(id) {
const place = draft[id];
place.childIds.forEach(deleteAllChildren);
delete draft[id];
}
});
}
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>
);
}
有时,您还可以通过将一些嵌套状态移动到子组件中来减少状态嵌套。 这适用于不需要存储的短暂 UI 状态,例如项目是否悬停。
回顾
- 如果两个状态变量总是一起更新,考虑将它们合并为一个。
- 仔细选择状态变量以避免产生“不可能”的状态。
- 以一种减少更新错误的可能性的方式来构建你的状态。
- 避免冗余和重复的状态,这样你就不需要保持同步。
- 除非你特别想阻止更新,否则不要将道具放入状态。
- 对于像选择这样的 UI 模式,将 ID 或索引保持在状态而不是对象本身。
- 如果更新深层嵌套状态很复杂,请尝试将其展平。
网友评论