构建状态的原则
当你编写一个包含某种状态的组件时,你必须选择使用多少个状态变量,以及它们的数据应该是什么形状。虽然即使状态结构欠佳,也可以编写正确的程序,但有一些原则可以指导您做出更好的选择:
- 组相关状态。如果始终同时更新两个或多个状态变量,请考虑将它们合并到单个状态变量中。
- 避免状态上的矛盾。当国家的结构方式使几个国家可能相互矛盾和“不同意”时,你就为错误留下了空间。尽量避免这种情况。
- 避免冗余状态。如果可以在渲染过程中从组件的 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> ) }
避免状态矛盾
这是一份带有和状态变量的酒店反馈表: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
true
由于 isSending
和 isSent
不应同时为 true
,因此最好将它们替换为一个状态
状态变量,该变量可能采用以下三种有效状态之一:(initial)、 和 :'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
,因此请将其从状态中删除。firstName
lastName
fullName
fullName
这是你如何做到的:
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
const fullName = firstName + ' ' + lastName;
因此,更改处理程序不需要执行任何特殊操作来更新它。当您调用 or 时,会触发重新渲染,然后根据新数据计算下一个。setFirstName
setLastName
fullName
避免状态重复
此菜单列表组件可让您从以下几种中选择一种旅行小吃:
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
虽然您也可以更新,但更简单的解决方法是删除重复项。在此示例中,您不是对象(创建与内部对象的重复项),而是保持 in 状态,然后通过在数组中搜索具有该 ID 的项来获取:selectedItem
selectedItem
items
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
重复消失了,你只保留了本质状态!
现在,如果您编辑所选项目,下面的消息将立即更新。这是因为会触发重新渲染,并会找到标题更新的项目。您不需要将所选项目保持在状态,因为只有所选 ID 是必需的。其余的可以在渲染期间计算。setItems
items.find(...)
REACT选择状态结构(2)https://developer.aliyun.com/article/1529474