良好地构建状态可以区分一个易于修改和调试的组件,以及一个经常产生错误的组件。以下是构建状态时应考虑的一些提示。
¥Structuring state well can make a difference between a component that is pleasant to modify and debug, and one that is a constant source of bugs. Here are some tips you should consider when structuring state.
你将学习到
-
何时使用单个与多个状态变量
¥When to use a single vs multiple state variables
-
组织状态时要避免什么
¥What to avoid when organizing state
-
如何解决状态结构的常见问题
¥How to fix common issues with the state structure
结构化状态的原则
¥Principles for structuring state
当你编写一个保存某些状态的组件时,你必须选择使用多少个状态变量以及它们的数据应该是什么形状。虽然即使使用次优状态结构也可以编写正确的程序,但有一些原则可以指导你做出更好的选择:
¥When you write a component that holds some state, you’ll have to make choices about how many state variables to use and what the shape of their data should be. While it’s possible to write correct programs even with a suboptimal state structure, there are a few principles that can guide you to make better choices:
-
分组相关状态。如果你总是同时更新两个或多个状态变量,请考虑将它们合并为一个状态变量。
¥Group related state. If you always update two or more state variables at the same time, consider merging them into a single state variable.
-
避免状态上的矛盾。当状态的结构使得多个状态可能相互矛盾并且 “有分歧” 时,你就会为错误留下空间。尽量避免这种情况。
¥Avoid contradictions in state. When the state is structured in a way that several pieces of state may contradict and “disagree” with each other, you leave room for mistakes. Try to avoid this.
-
避免冗余状态。如果你可以在渲染期间从组件的属性或其现有状态变量中计算出一些信息,则不应将该信息放入该组件的状态中。
¥Avoid redundant state. If you can calculate some information from the component’s props or its existing state variables during rendering, you should not put that information into that component’s state.
-
避免状态重复。当相同的数据在多个状态变量之间或嵌套对象中重复时,很难使它们保持同步。尽可能减少重复。
¥Avoid duplication in state. When the same data is duplicated between multiple state variables, or within nested objects, it is difficult to keep them in sync. Reduce duplication when you can.
-
避免深度嵌套状态。层次很深的状态更新起来不是很方便。如果可能,更优先以扁平的方式构建状态。
¥Avoid deeply nested state. Deeply hierarchical state is not very convenient to update. When possible, prefer to structure state in a flat way.
这些原则背后的目标是使状态易于更新而不会引入错误。从状态中删除冗余和重复数据有助于确保其所有部分保持同步。这类似于数据库工程师可能希望 “规范化” 数据库结构 减少出现错误的机会。套用爱因斯坦的话,“使你的状态尽可能简单 - 但不能更简单。“
¥The goal behind these principles is to make state easy to update without introducing mistakes. Removing redundant and duplicate data from state helps ensure that all its pieces stay in sync. This is similar to how a database engineer might want to “normalize” the database structure to reduce the chance of bugs. To paraphrase Albert Einstein, “Make your state as simple as it can be—but no simpler.”
现在让我们看看这些原则是如何应用到行动中的。
¥Now let’s see how these principles apply in action.
分组相关状态
¥Group related state
有时你可能不确定是使用单个还是多个状态变量。
¥You might sometimes be unsure between using a single or multiple state variables.
你应该这样做吗?
¥Should you do this?
const [x, setX] = useState(0);
const [y, setY] = useState(0);
或者这样?
¥Or this?
const [position, setPosition] = useState({ x: 0, y: 0 });
从技术上讲,你可以使用这些方法中的任何一种。但是,如果某些两个状态变量总是一起变化,那么将它们统一为单个状态变量可能是一个好主意。然后你就不会忘记始终保持它们同步,就像在这个例子中移动光标更新红点的两个坐标:
¥Technically, you can use either of these approaches. But if some two state variables always change together, it might be a good idea to unify them into a single state variable. Then you won’t forget to always keep them in sync, like in this example where moving the cursor updates both coordinates of the red dot:
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> ) }
将数据分组到对象或数组中的另一种情况是,当你不知道需要多少个状态时。例如,当你有一个用户可以添加自定义字段的表单时,它会很有帮助。
¥Another case where you’ll group data into an object or an array is when you don’t know how many pieces of state you’ll need. For example, it’s helpful when you have a form where the user can add custom fields.
避免状态矛盾
¥Avoid contradictions in state
这是带有 isSending
和 isSent
状态变量的酒店反馈表:
¥Here is a hotel feedback form with isSending
and isSent
state variables:
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
同时为 true
的情况。你的组件越复杂,就越难理解发生了什么。
¥While this code works, it leaves the door open for “impossible” states. For example, if you forget to call setIsSent
and setIsSending
together, you may end up in a situation where both isSending
and isSent
are true
at the same time. The more complex your component is, the harder it is to understand what happened.
由于 isSending
和 isSent
绝不能同时为 true
,因此最好将它们替换为一个可能采用三种有效状态之一的 status
状态变量:'typing'
(初始)、'sending'
和 'sent'
:
¥Since isSending
and isSent
should never be true
at the same time, it is better to replace them with one status
state variable that may take one of three valid states: 'typing'
(initial), 'sending'
, and '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); }); }
你仍然可以声明一些常量以提高可读性:
¥You can still declare some constants for readability:
const isSending = status === 'sending';
const isSent = status === 'sent';
但它们不是状态变量,因此你无需担心它们彼此不同步。
¥But they’re not state variables, so you don’t need to worry about them getting out of sync with each other.
避免冗余状态
¥Avoid redundant state
如果你可以在渲染期间从组件的属性或其现有状态变量中计算出一些信息,则不应将该信息放入该组件的状态中。
¥If you can calculate some information from the component’s props or its existing state variables during rendering, you should not put that information into that component’s state.
例如,拿这个表单来说。它有效,但你能在其中找到任何冗余状态吗?
¥For example, take this form. It works, but can you find any redundant state in it?
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
,因此将其从状态中移除。
¥This form has three state variables: firstName
, lastName
, and fullName
. However, fullName
is redundant. You can always calculate fullName
from firstName
and lastName
during render, so remove it from state.
你可以这样做:
¥This is how you can do it:
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
不是状态变量。而是,它是在渲染期间计算的:
¥Here, fullName
is not a state variable. Instead, it’s calculated during render:
const fullName = firstName + ' ' + lastName;
因此,更改处理程序不需要做任何特殊的事情来更新它。当你调用 setFirstName
或 setLastName
时,你会触发重新渲染,然后下一个 fullName
将根据新数据进行计算。
¥As a result, the change handlers don’t need to do anything special to update it. When you call setFirstName
or setLastName
, you trigger a re-render, and then the next fullName
will be calculated from the fresh data.
深入研究
¥Don’t mirror props in state
冗余状态的一个常见示例是这样的代码:
¥A common example of redundant state is code like this:
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
这里,color
状态变量被初始化为 messageColor
属性。问题是,如果父组件稍后传递不同的 messageColor
值(例如,'red'
而不是 'blue'
),则 color
状态变量将不会更新!状态仅在第一次渲染期间初始化。
¥Here, a color
state variable is initialized to the messageColor
prop. The problem is that if the parent component passes a different value of messageColor
later (for example, 'red'
instead of 'blue'
), the color
state variable would not be updated! The state is only initialized during the first render.
这就是为什么 “镜像” 状态变量中的某些属性会导致混淆。而是,直接在你的代码中使用 messageColor
属性。如果你想给它一个更短的名字,使用一个常量:
¥This is why “mirroring” some prop in a state variable can lead to confusion. Instead, use the messageColor
prop directly in your code. If you want to give it a shorter name, use a constant:
function Message({ messageColor }) {
const color = messageColor;
这样它就不会与从父组件传递的属性不同步。
¥This way it won’t get out of sync with the prop passed from the parent component.
只有当你想忽略特定属性的所有更新时,“镜像” 属性进入状态才有意义。按照惯例,属性名称以 initial
或 default
开头,以阐明其新值将被忽略:
¥“Mirroring” props into state only makes sense when you want to ignore all updates for a specific prop. By convention, start the prop name with initial
or default
to clarify that its new values are ignored:
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);
避免状态重复
¥Avoid duplication in state
此菜单列表组件可让你从多种旅行小吃中选择一种:
¥This menu list component lets you choose a single travel snack out of several:
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
的内容与 items
列表中的一项相同。这意味着有关条目本身的信息在两个地方重复。
¥Currently, it stores the selected item as an object in the selectedItem
state variable. However, this is not great: the contents of the selectedItem
is the same object as one of the items inside the items
list. This means that the information about the item itself is duplicated in two places.
为什么这是个问题?让我们让每个条目都可编辑:
¥Why is this a problem? Let’s make each item editable:
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
。
¥Notice how if you first click “Choose” on an item and then edit it, the input updates but the label at the bottom does not reflect the edits. This is because you have duplicated state, and you forgot to update selectedItem
.
尽管你也可以更新 selectedItem
,但更简单的解决方法是删除重复项。在此示例中,你将 selectedId
保持在状态中,而不是 selectedItem
对象(它与 items
中的对象创建重复),然后通过在 items
数组中搜索具有该 ID 的项目来获取 selectedItem
:
¥Although you could update selectedItem
too, an easier fix is to remove duplication. In this example, instead of a selectedItem
object (which creates a duplication with objects inside items
), you hold the selectedId
in state, and then get the selectedItem
by searching the items
array for an item with that ID:
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> </> ); }
状态曾经像这样被复制:
¥The state used to be duplicated like this:
-
items = [{ id: 0, title: 'pretzels'}, ...]
-
selectedItem = {id: 0, title: 'pretzels'}
但是改完之后是这样的:
¥But after the change it’s like this:
-
items = [{ id: 0, title: 'pretzels'}, ...]
-
selectedId = 0
重复没有了,你只保留本质状态!
¥The duplication is gone, and you only keep the essential state!
现在,如果你编辑所选条目,下面的消息将立即更新。这是因为 setItems
触发了重新渲染,而 items.find(...)
会找到具有更新标题的条目。你不需要将所选条目保持在状态中,因为只有所选 ID 是必需的。其余的可以在渲染期间计算。
¥Now if you edit the selected item, the message below will update immediately. This is because setItems
triggers a re-render, and items.find(...)
would find the item with the updated title. You didn’t need to hold the selected item in state, because only the selected ID is essential. The rest could be calculated during render.
避免深度嵌套状态
¥Avoid deeply nested state
想象一个由行星、大陆和国家组成的旅行计划。你可能会想使用嵌套对象和数组来构建其状态,如本例所示:
¥Imagine a travel plan consisting of planets, continents, and countries. You might be tempted to structure its state using nested objects and arrays, like in this example:
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: [] }] }] };
现在假设你要添加一个按钮来删除你已经访问过的地方。你会怎么做?更新嵌套状态 涉及从更改的部分一直向上复制对象。删除深度嵌套的位置将涉及复制其整个父位置链。这样的代码可能非常冗长。
¥Now let’s say you want to add a button to delete a place you’ve already visited. How would you go about it? Updating nested state involves making copies of objects all the way up from the part that changed. Deleting a deeply nested place would involve copying its entire parent place chain. Such code can be very verbose.
如果状态嵌套太多而不易更新,请考虑使其 “扁平”。这是你可以重组此数据的一种方法。你可以让每个地方保存其子地点 ID 的数组,而不是每个 place
都有其子地点数组的树状结构。然后存储一个从每个地点 ID 到对应地点的映射。
¥If the state is too nested to update easily, consider making it “flat”. Here is one way you can restructure this data. Instead of a tree-like structure where each place
has an array of its child places, you can have each place hold an array of its child place IDs. Then store a mapping from each place ID to the corresponding place.
这种数据重组可能会让你想起看到的数据库表:
¥This data restructuring might remind you of seeing a database table:
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: [] } };
现在状态为 “扁平”(也称为 “规范化”),更新嵌套项变得更加容易。
¥Now that the state is “flat” (also known as “normalized”), updating nested items becomes easier.
为了现在删除一个地方,你只需要更新两个级别的状态:
¥In order to remove a place now, you only need to update two levels of state:
-
其父位置的更新版本应从其
childIds
数组中排除已删除的 ID。¥The updated version of its parent place should exclude the removed ID from its
childIds
array. -
根 “表格” 对象的更新版本应该包括父位置的更新版本。
¥The updated version of the root “table” object should include the updated version of the parent place.
下面是一个你可以如何去做的例子:
¥Here is an example of how you could go about it:
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> ); }
你可以任意嵌套状态,但是将它设为 “扁平” 可以解决很多问题。它使状态更容易更新,并有助于确保你不会在嵌套对象的不同部分出现重复。
¥You can nest state as much as you like, but making it “flat” can solve numerous problems. It makes state easier to update, and it helps ensure you don’t have duplication in different parts of a nested object.
深入研究
¥Improving memory usage
理想情况下,你还可以从 “表格” 对象中删除已删除的项目(及其子项!)以提高内存使用率。这个版本就是这样做的。它还 使用 Immer 使更新逻辑更加简洁。
¥Ideally, you would also remove the deleted items (and their children!) from the “table” object to improve memory usage. This version does that. It also uses Immer to make the update logic more concise.
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
有时,你还可以通过将一些嵌套状态移动到子组件中来减少状态嵌套。这适用于不需要存储的短暂 UI 状态,例如条目是否悬停。
¥Sometimes, you can also reduce state nesting by moving some of the nested state into the child components. This works well for ephemeral UI state that doesn’t need to be stored, like whether an item is hovered.
回顾
-
如果两个状态变量总是一起更新,考虑将它们合并为一个。
¥If two state variables always update together, consider merging them into one.
-
仔细选择状态变量以避免创建 “不可能的” 状态。
¥Choose your state variables carefully to avoid creating “impossible” states.
-
以一种减少更新错误的可能性的方式来构建你的状态。
¥Structure your state in a way that reduces the chances that you’ll make a mistake updating it.
-
避免冗余和重复状态,这样你就不需要保持同步。
¥Avoid redundant and duplicate state so that you don’t need to keep it in sync.
-
除非你特别想阻止更新,否则不要将属性放入状态。
¥Don’t put props into state unless you specifically want to prevent updates.
-
对于像选择这样的 UI 模式,将 ID 或索引保持在状态而不是对象本身。
¥For UI patterns like selection, keep ID or index in state instead of the object itself.
-
如果更新深层嵌套状态很复杂,请尝试将其展平。
¥If updating deeply nested state is complicated, try flattening it.
挑战 1 / 4: 修复不更新的组件
¥Fix a component that’s not updating
这个 Clock
组件接收两个属性:color
和 time
。当你在选择框中选择不同的颜色时,Clock
组件会从其父组件接收到不同的 color
属性。但是,由于某种原因,显示的颜色不会更新。为什么?解决问题。
¥This Clock
component receives two props: color
and time
. When you select a different color in the select box, the Clock
component receives a different color
prop from its parent component. However, for some reason, the displayed color doesn’t update. Why? Fix the problem.
import { useState } from 'react'; export default function Clock(props) { const [color, setColor] = useState(props.color); return ( <h1 style={{ color: color }}> {props.time} </h1> ); }