结构化状态的原则
结构化状态的原则
当您编写一个保存某些状态的组件时,您必须选择使用多少个状态变量以及它们的数据应该是什么形状。虽然即使使用次优状态结构也可以编写正确的程序,但有一些原则可以指导您做出更好的选择:
1.组相关状态。如果您总是同时更新两个或多个状态变量,请考虑将它们合并为一个状态变量。
2.避免状态上的矛盾。当状态的结构方式使多个状态可能相互矛盾和“不一致”时,就会为错误留下空间。尽量避免这种情况。
3.避免冗余状态。如果您可以在渲染期间从组件的 props 或其现有状态变量中计算出一些信息,则不应将该信息放入该组件的状态中。
4.避免状态重复。当相同的数据在多个状态变量之间或嵌套对象中重复时,很难使它们保持同步。尽可能减少重复。
5.避免深度嵌套状态。层次很深的状态更新起来不是很方便。如果可能,更喜欢以扁平的方式构建状态。
这些原则背后的目标是使状态易于更新而不会引入错误。从状态中删除冗余和重复数据有助于确保其所有部分保持同步。这类似于数据库工程师可能希望“规范化”数据库结构以减少出现错误的可能性。套用阿尔伯特·爱因斯坦的话,“让你的状态尽可能简单——但不要更简单。”
现在让我们看看这些原则是如何应用到行动中的。
1.组相关状态
有时您可能不确定是使用单个还是多个状态变量。
你应该这样做吗?
const [x, setX] = useState(0);
const [y, setY] = useState(0);
或者这个?
const [position, setPosition] = useState({ x: 0, y: 0 });
从技术上讲,您可以使用这些方法中的任何一种。但是,如果某些两个状态变量总是一起变化,那么将它们统一为一个状态变量可能是个好主意。然后你不会忘记始终保持它们同步.
2.避免状态矛盾
isSending这是带有状态变量的酒店反馈表isSent:
const [text, setText] = useState(''); const [isSending, setIsSending] = useState(false); const [isSent, setIsSent] = useState(false);
虽然此代码有效,但它为“不可能”状态敞开了大门。这三种状态是不可能同时存在的,如果你在修改了其中一个状态后忘记修改同步其他的状态就会出现问题。 由于isSending和isSent永远不应该true同时存在,因此最好将它们替换为一个状态变量,该变量可能采用三种有效状态status之一 'typing':(初始)、'sending'和'sent'
3.避免冗余状态
如果您可以在渲染期间从组件的 props 或其现有状态变量中计算出一些信息,则不应将该信息放入该组件的状态中。 例如:
export default function Form(){ const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [fullName, setFullName] = useState(''); retutun ( <p> Your ticket will be issued to: <b>{fullName}</b> </p> ) }
此表单具有三个状态变量:firstName、lastName和fullName。然而,fullName是多余的。您始终可以fullName在渲染期间firstName和lastName渲染期间进行计算,因此将其从状态中删除。
export default function Form(){ const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const fullName = firstName + ' ' + lastName; retutun ( <p> Your ticket will be issued to: <b>{fullName}</b> </p> ) }
注意:不要在状态中镜像道具 冗余状态的一个常见示例是这样的代码:
function Message({ messageColor }) { const [color, setColor] = useState(messageColor);
这里,color状态变量被初始化为messageColorprop。问题在于,如果父组件传递了不同的 later 值messageColor(例如,'red'而不是'blue'),color 状态变量将不会更新!状态仅在第一次渲染期间初始化。
这就是为什么在状态变量中“镜像”某些 prop 会导致混淆。相反,messageColor直接在您的代码中使用 prop。如果你想给它一个更短的名字,使用一个常量:
function Message({ messageColor }) { const color = messageColor;
这样它就不会与从父组件传递的 prop 不同步。
仅当您想忽略特定道具的所有更新时,将道具“镜像”到状态中才有意义。按照惯例,prop 名称以initialor开头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);
4.避免状态重复
看下面的例子
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。这意味着有关项目本身的信息在两个地方重复。
请注意,如果您先在项目上单击“选择”然后对其进行编辑,输入会更新,但底部的标签不会反映编辑内容。这是因为你有重复的状态,而你忘记了更新selectedItem。
虽然您selectedItem也可以更新,但更简单的解决方法是删除重复项。在此示例中,您持有in 状态,而不是selectedItem对象(它创建了内部对象的重复项items) ,然后通过在数组中搜索具有该 ID 的项目来获取:selectedId 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 [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: 'pretzels'}, ...] selectedItem = {id: 0, title: 'pretzels'} 但是改完之后是这样的:
items = [{ id: 0, title: 'pretzels'}, ...] selectedId = 0 重复没有了,你只保留本质状态!
现在,如果您编辑所选项目,下面的消息将立即更新。这是因为setItems触发重新渲染,并items.find(...)会找到具有更新标题的项目。您不需要将所选项目保持在状态中,因为只有所选 ID是必需的。其余的可以在渲染期间计算。
5.避免深度嵌套状态
当我们遇到一个树形结构,让我们删除用户点击的其中一个子组件的内容时,我们会怎么做?
更新嵌套状态涉及从更改的部分一直向上复制对象。删除深度嵌套的位置将涉及复制其整个父位置链。这样的代码可能非常冗长。
如果状态嵌套太多而不易更新,请考虑使其“扁平化”。这是您可以重组此数据的一种方法。您可以让每个地点都包含其子place地点 ID的数组,而不是每个地点都有一个子地点数组的树状结构。然后存储一个从每个地点ID到对应地点的映射。
例如我们可以将这样的嵌套数组对象
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: [] }] }] };
扁平化成下面的数据数据的扁平化看我一以前分享的文章 https://www.cnblogs.com/ximenchuifa/p/17244323.html 现在状态是“平坦的”(也称为“规范化”),更新嵌套项变得更容易。
为了现在删除一个地方,你只需要更新两个级别的状态:
其父位置的更新版本应从其数组中排除已删除的 ID childIds。 根“表”对象的更新版本应包括父位置的更新版本。 下面是一个你可以如何去做的例子:
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]; } }); } ... );}