參考文章
選擇 State 結構
構建良好的 state 可以讓組件變得易于修改和調試,而不會經常出錯。以下是在構建 state 時應該考慮的一些建議。
構建 state 的原則
當編寫一個存有 state 的組件時,需要選擇使用多少個 state 變量以及它們都是怎樣的數(shù)據格式。盡管選擇次優(yōu)的 state 結構下也可以編寫正確的程序,但有幾個原則可以指導做出更好的決策:
- 合并關聯(lián)的 state。如果總是同時更新兩個或更多的 state 變量,請考慮將它們合并為一個單獨的 state 變量。
- 避免互相矛盾的 state。當 state 結構中存在多個相互矛盾或“不一致”的 state 時,就可能為此會留下隱患。應盡量避免這種情況。
- 避免冗余的 state。如果能在渲染期間從組件的 props 或其現(xiàn)有的 state 變量中計算出一些信息,則不應將這些信息放入該組件的 state 中。
- 避免重復的 state。當同一數(shù)據在多個 state 變量之間或在多個嵌套對象中重復時,這會很難保持它們同步。應盡可能減少重復。
- 避免深度嵌套的 state。深度分層的 state 更新起來不是很方便。如果可能的話,最好以扁平化方式構建 state。
這些原則背后的目標是 使 state 易于更新而不引入錯誤。從 state 中刪除冗余和重復數(shù)據有助于確保所有部分保持同步。這類似于數(shù)據庫工程師想要 “規(guī)范化”數(shù)據庫結構,以減少出現(xiàn)錯誤的機會。用愛因斯坦的話說,“讓你的狀態(tài)盡可能簡單,但不要過于簡單?!?/strong>
現(xiàn)在讓我們來看看這些原則在實際中是如何應用的。
合并關聯(lián)的 state
有時候可能會不確定是使用單個 state 變量還是多個 state 變量。
你會像下面這樣做嗎?
const [x, setX] = useState(0);
const [y, setY] = useState(0);
或這樣?
const [position, setPosition] = useState({ x: 0, y: 0 });
從技術上講,可以使用其中任何一種方法。但是,如果某兩個 state 變量總是一起變化,則將它們統(tǒng)一成一個 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>
)
}
另一種情況是,將數(shù)據整合到一個對象或一個數(shù)組中時,不知道需要多少個 state 片段。例如,當有一個用戶可以添加自定義字段的表單時,這將會很有幫助。
注意:如果 state 變量是一個對象時,請記住,不能只更新其中的一個字段 而不顯式復制其他字段。例如,在上面的例子中,不能寫成 setPosition({ x: 100 })
,因為它根本就沒有 y
屬性! 相反,如果想要僅設置 x
,則可執(zhí)行 setPosition({ ...position, x: 100 })
,或將它們分成兩個 state 變量,并執(zhí)行 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>
);
}
// 假裝發(fā)送一條消息。
function sendMessage(text) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}
盡管這段代碼是有效的,但也會讓一些 state “極難處理”。例如,如果忘記同時調用 setIsSent
和 setIsSending
,則可能會出現(xiàn) isSending
和 isSent
同時為 true
的情況。組件越復雜,就越難理解發(fā)生了什么。
因為 isSending
和 isSent
不應同時為 true
,所以最好用一個 status
變量來代替它們,這個 state 變量可以采取三種有效狀態(tài)其中之一:'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>
);
}
// 假裝發(fā)送一條消息。
function sendMessage(text) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}
仍然可以聲明一些常量,以提高可讀性:
const isSending = status === 'sending';
const isSent = status === 'sent';
但它們不是 state 變量,所以不必擔心它們彼此失去同步。
避免冗余的 state
如果能在渲染期間從組件的 props 或其現(xiàn)有的 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
時,會觸發(fā)一次重新渲染,然后下一個 fullName
將從新數(shù)據中計算出來。
避免重復的 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
對象中(它創(chuàng)建了一個與 items
內重復的對象),然后 通過搜索 items
數(shù)組中具有該 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!
現(xiàn)在,如果編輯 selected 元素,下面的消息將立即更新。這是因為 setItems
會觸發(fā)重新渲染,而 items.find(...)
會找到帶有更新文本的元素。不需要在 state 中保存 選定的元素,因為只有 選定的 ID 是必要的。其余的可以在渲染期間計算。
避免深度嵌套的 state
想象一下,一個由行星、大陸和國家組成的旅行計劃??赡軙L試使用嵌套對象和數(shù)組來構建它的 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: []
}]
}]
};
現(xiàn)在,假設想添加一個按鈕來刪除一個已經去過的地方。會怎么做呢?更新嵌套的 state 需要從更改部分一直向上復制對象。刪除一個深度嵌套的地點將涉及復制其整個父級地點鏈。這樣的代碼可能非常冗長。
如果 state 嵌套太深,難以輕松更新,可以考慮將其“扁平化”。 這里有一個方法可以重構上面這個數(shù)據。不同于樹狀結構,每個節(jié)點的 place
都是一個包含 其子節(jié)點 的數(shù)組,可以讓每個節(jié)點的 place
作為數(shù)組保存 其子節(jié)點的 ID。然后存儲一個節(jié)點 ID 與相應節(jié)點的映射關系。
這個數(shù)據重組可能會讓你想起看到一個數(shù)據庫表:
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: []
},
};
現(xiàn)在 state 已經“扁平化”(也稱為“規(guī)范化”),更新嵌套項會變得更加容易。
現(xiàn)在要刪除一個地點,只需要更新兩個 state 級別:
- 其 父級 地點的更新版本應該從其
childIds
數(shù)組中排除已刪除的 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];
// 創(chuàng)建一個其父級地點的新版本
// 但不包括子級 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 更容易更新,并且有助于確保在嵌套對象的不同部分中沒有重復。文章來源:http://www.zghlxwxcb.cn/news/detail-689803.html
有時候,也可以通過將一些嵌套 state 移動到子組件中來減少 state 的嵌套。這對于不需要保存的短暫 UI 狀態(tài)非常有效,比如一個選項是否被懸停。文章來源地址http://www.zghlxwxcb.cn/news/detail-689803.html
摘要
- 如果兩個 state 變量總是一起更新,請考慮將它們合并為一個。
- 仔細選擇 state 變量,以避免創(chuàng)建“極難處理”的 state。
- 用一種減少出錯更新的機會的方式來構建 state。
- 避免冗余和重復的 state,這樣就不需要保持同步。
- 除非特別想防止更新,否則不要將 props 放入 state 中。
- 對于選擇類型的 UI 模式,請在 state 中保存 ID 或索引而不是對象本身。
- 如果深度嵌套 state 更新很復雜,請嘗試將其展開扁平化。
到了這里,關于React 18 選擇 State 結構的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!