参考文章
选择 State 结构
构建良好的 state 可以让组件变得易于修改和调试,而不会经常出错。以下是在构建 state 时应该考虑的一些建议。
构建 state 的原则
当编写一个存有 state 的组件时,需要选择使用多少个 state 变量以及它们都是怎样的数据格式。尽管选择次优的 state 结构下也可以编写正确的程序,但有几个原则可以指导做出更好的决策:
- 合并关联的 state。如果总是同时更新两个或更多的 state 变量,请考虑将它们合并为一个单独的 state 变量。
- 避免互相矛盾的 state。当 state 结构中存在多个相互矛盾或“不一致”的 state 时,就可能为此会留下隐患。应尽量避免这种情况。
- 避免冗余的 state。如果能在渲染期间从组件的 props 或其现有的 state 变量中计算出一些信息,则不应将这些信息放入该组件的 state 中。
- 避免重复的 state。当同一数据在多个 state 变量之间或在多个嵌套对象中重复时,这会很难保持它们同步。应尽可能减少重复。
- 避免深度嵌套的 state。深度分层的 state 更新起来不是很方便。如果可能的话,最好以扁平化方式构建 state。
这些原则背后的目标是 使 state 易于更新而不引入错误。从 state 中删除冗余和重复数据有助于确保所有部分保持同步。这类似于数据库工程师想要 “规范化”数据库结构,以减少出现错误的机会。用爱因斯坦的话说,“让你的状态尽可能简单,但不要过于简单。”
现在让我们来看看这些原则在实际中是如何应用的。
合并关联的 state
有时候可能会不确定是使用单个 state 变量还是多个 state 变量。
你会像下面这样做吗?
const [x, setX] = useState(0);
const [y, setY] = useState(0);
或这样?
const [position, setPosition] = useState({ x: 0, y: 0 });
从技术上讲,可以使用其中任何一种方法。但是,如果某两个 state 变量总是一起变化,则将它们统一成一个 state 变量可能更好。这样就不会忘记让它们始终保持同步,就像下面这个例子中,移动光标会同时更新红点的两个坐标:
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>
)
}
另一种情况是,将数据整合到一个对象或一个数组中时,不知道需要多少个 state 片段。例如,当有一个用户可以添加自定义字段的表单时,这将会很有帮助。
注意:如果 state 变量是一个对象时,请记住,不能只更新其中的一个字段 而不显式复制其他字段。例如,在上面的例子中,不能写成 setPosition({ x: 100 })
,因为它根本就没有 y
属性! 相反,如果想要仅设置 x
,则可执行 setPosition({ ...position, x: 100 })
,或将它们分成两个 state 变量,并执行 setX(100)
。
避免矛盾的 state
下面是带有 isSending
和 isSent
两个 state 变量的酒店反馈表单:
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 “极难处理”。例如,如果忘记同时调用 setIsSent
和 setIsSending
,则可能会出现 isSending
和 isSent
同时为 true
的情况。组件越复杂,就越难理解发生了什么。
因为 isSending
和 isSent
不应同时为 true
,所以最好用一个 status
变量来代替它们,这个 state 变量可以采取三种有效状态其中之一:'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>
);
}
// 假装发送一条消息。
function sendMessage(text) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}
仍然可以声明一些常量,以提高可读性:
const isSending = status === 'sending';
const isSent = status === 'sent';
但它们不是 state 变量,所以不必担心它们彼此失去同步。
避免冗余的 state
如果能在渲染期间从组件的 props 或其现有的 state 变量中计算出一些信息,则不应该把这些信息放到该组件的 state 中。
例如,以这个表单为例。它可以运行,但你能找到其中任何冗余的 state 吗?
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>
</>
);
}
这个表单有三个 state 变量:firstName
、lastName
和 fullName
。然而,fullName
是多余的。在渲染期间,始终可以从 firstName
和 lastName
中计算出 fullName
,因此需要把它从 state 中删除。
可以这样做:
import { useState } from 'react';
export default function Form() {
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);
}
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>
</>
);
}
这里的 fullName
不是 一个 state 变量。相反,它是在渲染期间中计算出的:
const fullName = firstName + ' ' + lastName;
因此,更改处理程序不需要做任何特殊操作来更新它。当调用 setFirstName
或 setLastName
时,会触发一次重新渲染,然后下一个 fullName
将从新数据中计算出来。
避免重复的 state
下面这个菜单列表组件可以让你在多种旅行小吃中选择一个:
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 变量中。然而,这并不好:selectedItem
的内容与 items
列表中的某个项是同一个对象。 这意味着关于该项本身的信息在两个地方产生了重复。
为什么这是个问题?让我们使每个项目都可以编辑:
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
:
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 元素,下面的消息将立即更新。这是因为 setItems
会触发重新渲染,而 items.find(...)
会找到带有更新文本的元素。不需要在 state 中保存 选定的元素,因为只有 选定的 ID 是必要的。其余的可以在渲染期间计算。
避免深度嵌套的 state
想象一下,一个由行星、大陆和国家组成的旅行计划。可能会尝试使用嵌套对象和数组来构建它的 state,就像下面这个例子:
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>
</>
);
}
// 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: 10,
title: 'Americas',
childPlaces: [{
id: 18,
title: 'Venezuela',
childPlaces: []
}]
}, {
id: 19,
title: 'Asia',
childPlaces: [{
id: 20,
title: 'China',
childPlaces: []
}]
}, {
id: 26,
title: 'Europe',
childPlaces: [{
id: 27,
title: 'Croatia',
childPlaces: [],
}, {
id: 33,
title: 'Turkey',
childPlaces: [],
}]
}, {
id: 34,
title: 'Oceania',
childPlaces: [{
id: 35,
title: 'Australia',
childPlaces: [],
}]
}]
}, {
id: 42,
title: 'Moon',
childPlaces: [{
id: 43,
title: 'Rheita',
childPlaces: []
}]
}]
};
现在,假设想添加一个按钮来删除一个已经去过的地方。会怎么做呢?更新嵌套的 state 需要从更改部分一直向上复制对象。删除一个深度嵌套的地点将涉及复制其整个父级地点链。这样的代码可能非常冗长。
如果 state 嵌套太深,难以轻松更新,可以考虑将其“扁平化”。 这里有一个方法可以重构上面这个数据。不同于树状结构,每个节点的 place
都是一个包含 其子节点 的数组,可以让每个节点的 place
作为数组保存 其子节点的 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>
</>
);
}
// places.js
export const initialTravelPlan = {
0: {
id: 0,
title: '(Root)',
childIds: [1, 42],
},
1: {
id: 1,
title: 'Earth',
childIds: [2, 10, 19, 26, 34]
},
2: {
id: 2,
title: 'Africa',
childIds: [3, 4]
},
3: {
id: 3,
title: 'Botswana',
childIds: []
},
4: {
id: 4,
title: 'Egypt',
childIds: []
},
10: {
id: 10,
title: 'Americas',
childIds: [18],
},
18: {
id: 18,
title: 'Venezuela',
childIds: []
},
19: {
id: 19,
title: 'Asia',
childIds: [20],
},
20: {
id: 20,
title: 'China',
childIds: []
},
26: {
id: 26,
title: 'Europe',
childIds: [27, 33],
},
27: {
id: 27,
title: 'Croatia',
childIds: []
},
33: {
id: 33,
title: 'Turkey',
childIds: []
},
34: {
id: 34,
title: 'Oceania',
childIds: [35],
},
35: {
id: 35,
title: 'Australia',
childIds: []
},
42: {
id: 42,
title: 'Moon',
childIds: [43]
},
43: {
id: 43,
title: 'Rheita',
childIds: []
},
};
现在 state 已经“扁平化”(也称为“规范化”),更新嵌套项会变得更加容易。
现在要删除一个地点,只需要更新两个 state 级别:
- 其 父级 地点的更新版本应该从其
childIds
数组中排除已删除的 ID。 - 其根级“表”对象的更新版本应包括父级地点的更新版本。
下面是展示如何处理它的一个示例:
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>
);
}
确实可以随心所欲地嵌套 state,但是将其“扁平化”可以解决许多问题。这使得 state 更容易更新,并且有助于确保在嵌套对象的不同部分中没有重复。文章来源:https://www.toymoban.com/news/detail-689803.html
有时候,也可以通过将一些嵌套 state 移动到子组件中来减少 state 的嵌套。这对于不需要保存的短暂 UI 状态非常有效,比如一个选项是否被悬停。文章来源地址https://www.toymoban.com/news/detail-689803.html
摘要
- 如果两个 state 变量总是一起更新,请考虑将它们合并为一个。
- 仔细选择 state 变量,以避免创建“极难处理”的 state。
- 用一种减少出错更新的机会的方式来构建 state。
- 避免冗余和重复的 state,这样就不需要保持同步。
- 除非特别想防止更新,否则不要将 props 放入 state 中。
- 对于选择类型的 UI 模式,请在 state 中保存 ID 或索引而不是对象本身。
- 如果深度嵌套 state 更新很复杂,请尝试将其展开扁平化。
到了这里,关于React 18 选择 State 结构的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!