網(wǎng)站首頁 編程語言 正文
引言
本文源于翻譯 Avoid These Common Pitfalls Of React useState 由公眾號KooFE前端團隊完成翻譯
useState 是我們使用最頻繁的 React hook,在代碼中隨處可見,但是也經(jīng)常會出現(xiàn)一些錯誤的用法。
或許你已經(jīng)經(jīng)歷過這些錯誤的用法,但是可能還沒有意識到這是錯誤,比如寫出了一些冗余的、重復的、矛盾的 state,讓你不得不額外使用 useEffect 來處理它們。由于這些錯誤用法的存在,會讓代碼的可讀性變差,提高了代碼的維護成本。
了解這些易犯的錯誤,可以讓我們獲得如下收益:
- 代碼更容易閱讀和維護
- 減少代碼出 Bug 的可能性
- 降低代碼的復雜程度
在本文中,將介紹一些關于 useState 的常見錯誤,以便在今后的工作中避免這些錯誤。
冗余的 state
對于初級開發(fā)者來說,定義和使用冗余的 state 是一個比較常見的錯誤。如果一個 state 依賴了另外一個 state,就是這種典型的錯誤用法。
簡單示例
下面是一個簡單的組件,允許用戶編輯自己的姓名,其中第一個輸入框是用戶的姓氏,后一個輸入框是用戶的名字,然后將姓名組合在一起渲染在輸入框的下面。
代碼實現(xiàn)如下:
import { useState } from "react";
function RedundantState() {
const [firstName, setFirstName] = useState(""); // 姓氏
const [lastName, setLastName] = useState(""); // 名字
const [fullName, setFullName] = useState(""); // 姓名
const onChangeFirstName = (event) => {
setFirstName(event.target.value);
setFullName(`${event.target.value} ${lastName}`);
};
const onChangeLastName = (event) => {
setLastName(event.target.value);
setFullName(`${firstName} ${event.target.value}`);
};
return (
<>
<form>
<input
value={firstName}
onChange={onChangeFirstName}
placeholder="First Name"
/>
<input
value={lastName}
onChange={onChangeLastName}
placeholder="Last Name"
/>
</form>
<div>Full name: {fullName}</div>
</>
);
}
很明顯,這段代碼中的 fullName 是冗余的 state
問題分析
可能你會說,先后依次更新 firstName 和 fullName 會導致額外的渲染周期。
const onChangeFirstName = (event) => {
setFirstName(event.target.value);
setFullName(`${event.target.value} ${lastName}`);
};
但是,React state 的更新是批量更新,所以不會為每個 state 更新做單獨的渲染。
因此,在大多數(shù)情況下,性能方面的差異不大。問題在于可維護性和引入錯誤的風險。讓我們再次看一下示例代碼:
const onChangeFirstName = (event) => {
setFirstName(event.target.value);
setFullName(`${event.target.value} ${lastName}`);
};
const onChangeLastName = (event) => {
setLastName(event.target.value);
setFullName(`${firstName} ${event.target.value}`);
};
每次更新firstName 或 lastName 時,我們都必須要更新 fullName。在更復雜的場景中,這很容易被遺漏。因此,這會導致代碼更難重構(gòu),引入 bug 的可能性也會增加。
如前所述,在大多數(shù)情況下,我們不必擔心性能。但是,如果被依賴的 state 是大型的數(shù)組或需要大量的計算,則可以使用 useMemo 來做優(yōu)化處理。
解決方案
fullName 可以由 firstName 和 lastName 直接拼接而成。
export function RedundantState() {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const fullName = `${firstName} ${lastName}`;
...
return (
<>
<form>
...
</form>
<div>Full name: {fullName}</div>
</>
);
}
重復的 state
在多個 state 中存在重復的數(shù)據(jù),也是一個比較常見的錯誤。通常在做數(shù)據(jù)的轉(zhuǎn)換、排序或過濾時會遇到這種情況。另一種常見情況是選擇展示不同的數(shù)據(jù),比如接下來介紹的例子。
簡單示例
這個組件用于顯示項目列表,用戶可以單擊相應的按鈕來打開 modal 彈窗。
在下面的代碼中就存在這種錯誤用法。
import { useState } from "react";
// const items = [
// {
// id: "item-1",
// text: "Item 1",
// },
// ...
// ]
function DuplicateState({ items }) {
const [selectedItem, setSelectedItem] = useState();
const onClickItem = (item) => {
setSelectedItem(item);
};
return (
<>
{selectedItem && <Modal item={selectedItem} />}
<ul>
{items.map((row) => (
<li key={row.id}>
{row.text}
<button onClick={() => onClickItem(row)}>Open</button>
</li>
))}
</ul>
</>
);
}
這段代碼中的問題是,將 item 原封不動地拷貝到了 state 中。
問題分析
在上面的代碼中,這種重復的數(shù)據(jù)違反了單一數(shù)據(jù)源原則。事實上,一旦用戶選擇了任何一項,我們就會出現(xiàn)兩個數(shù)據(jù)源:selectedItem 狀態(tài)和 items 數(shù)組中的數(shù)據(jù)。
假如,用戶能夠在 modal 彈窗中編輯這些數(shù)據(jù)。可能會是這樣的:
- 用戶在 modal 彈窗中更改數(shù)據(jù)并提交
- 將向服務器發(fā)送請求并更新數(shù)據(jù)庫中的 item
- 前端更新 item 數(shù)據(jù)(通過服務器的響應或重新請求 items 列表)。
- 前端使用新的 items 數(shù)組重新渲染。
- 現(xiàn)在的問題是:DuplicateState 組件內(nèi)部發(fā)生了什么?
這就是問題所在。selectedItem 狀態(tài)仍將包含舊數(shù)據(jù)。它將不同步。你可以想象,在更復雜的情況下,這可能會成為一個令人討厭的 bug。
當然,我們可以寫代碼來實現(xiàn) selectedItem 狀態(tài)同步。但我們不得不使用 useEffect 來監(jiān)聽 items 數(shù)組中的變化。
解決方案
一個更簡單的解決方案是只跟蹤選定的 id。正如你所看到的,該解決方案 “冗余的 state” 部分中的解決方案非常相似:我們只需從 id 中計算出 selectedItem 變量。
// const items = [
// {
// id: "item-1",
// text: "Item 1",
// },
// ...
// ]
function DuplicateState({ items }) {
const [selectedItemId, setSelectedItemId] = useState();
const selectedItem = items.find(({ id }) => id === selectedItemId);
const onClickItem = (itemId) => {
setSelectedItemId(itemId);
};
return (
<>
{selectedItem && <Modal item={selectedItem} />}
<ul>
{items.map((row) => (
<li key={row.id}>
{row.text}
<button onClick={() => onClickItem(row.id)}>Open</button>
</li>
))}
</ul>
</>
);
}
使用 useEffect 更新 state
另一個常見問題是使用 useEffect 來監(jiān)聽變量的變化。
簡單示例
我們繼續(xù)使用上一節(jié)的示例:
在組件中,當 items 發(fā)生變化后,使用 useEffect 同步給 selectedItem。
import { useEffect, useState } from "react";
// const items = [
// {
// id: "item-1",
// text: "Item 1",
// },
// ...
// ]
function DuplicateState({ items }) {
const [selectedItem, setSelectedItem] = useState();
useEffect(() => {
if (selectedItem) {
setSelectedItem(items.find(({ id }) => id === selectedItem.id));
}
}, [items]);
const onClickItem = (item) => {
setSelectedItem(item);
};
return (
<>
{selectedItem && <Modal item={selectedItem} />}
<ul>
{items.map((row) => (
<li key={row.id}>
{row.text}
<button onClick={() => onClickItem(row)}>Open</button>
</li>
))}
</ul>
</>
);
}
這段代碼能夠正常工作,并同步保持 selectedItem 狀態(tài)。是不是覺得它的實現(xiàn)方式有點 hack?
問題分析
這種方法存在多個問題:
- useEffect 不容易閱讀和理解。因此,使用 useEffect 的次數(shù)越少越好。
- 在 useEffect 中更新 state 會導致額外的渲染。雖然不會引起性能方面的大問題,但也需要考慮。
- 在代碼中,我們在 selectedItem 狀態(tài)和 items 屬性之間引入了某種隱藏的關系。在閱讀或更改代碼時,這很容易錯過。
- 在正確的時間觸發(fā) useEffect 中的代碼可能很困難。在這種模式中,我們經(jīng)常要額外引入其他解決方法,例如避免在第一次渲染時運行代碼。下面是一個示例:
function DuplicateState({ items }) {
const [selectedItem, setSelectedItem] = useState();
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
setSelectedItem(items.find(({ id }) => id === selectedItem.id));
}, [items]);
...
如果你想使用 useEffect 或在另一個開發(fā)人員的代碼中看到它,問問自己是否真的需要它。也許可以通過前面介紹的方法來避免這種情況。
解決方案
您可能已經(jīng)猜到了:上一節(jié)的解決方案也幫助我們刪除 useEffect。如果我們只存儲所選項目的 ID 而不是整個對象,那么就沒有什么可同步的。
import { useState } from "react";
// const items = [
// {
// id: "item-1",
// text: "Item 1",
// },
// ...
// ]
function DuplicateState({ items }) {
const [selectedItemId, setSelectedItemId] = useState();
const selectedItem = items.find(({ id }) => id === selectedItemId);
const onClickItem = (id) => {
setSelectedItem(id);
};
return (
<>
{selectedItem && <Modal item={selectedItem} />}
<ul>
{items.map((row) => (
<li key={row.id}>
{row.text}
<button onClick={() => onClickItem(row.id)}>Open</button>
</li>
))}
</ul>
</>
);
}
使用 useEffect 監(jiān)聽 state 變化
與上一節(jié)相關的另外一個常見問題是使用 useEffect 對狀態(tài)的變化做出反應。但解決方案略有不同。
簡單示例
這是一個顯示產(chǎn)品的組件。用戶可以通過單擊按鈕顯示或隱藏產(chǎn)品詳細信息。無論何時顯示或隱藏產(chǎn)品信息,我們都會觸發(fā)一個動作(在本例中,會觸發(fā)一個埋點數(shù)據(jù)上報)。
import { useEffect, useState } from "react";
function ProductView({ name, details }) {
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
useEffect(() => {
trackEvent({ event: "Toggle Product Details", value: isDetailsVisible });
}, [isDetailsVisible]);
const toggleDetails = () => {
setIsDetailsVisible(!isDetailsVisible);
};
return (
<div>
{name}
<button onClick={toggleDetails}>Show details</button>
{isDetailsVisible && <ProductDetails {...details} />}
</div>
);
}
代碼中的 useEffect 會偵聽 isDetailsVisible 是否變化,并相應地觸發(fā)埋點事件。
問題分析
代碼中的問題如下:
- useEffect通常不容易理解。
- 它可能會導致不必要的渲染周期(如果在效果內(nèi)部更新了狀態(tài))。
- 很容易引入與渲染生命周期相關的錯誤。事實上,這段代碼在初始渲染期間運行trackEvent,這會導致一個 bug。
- 它將影響與實際原因分開。在這段代碼中,我們看到 trackEvent 正在運行,是因為 isDetailsVisible 發(fā)生了更改。但真正的原因是用戶按下了 “顯示詳細信息” 按鈕。
解決方案
在許多情況下,可以刪除用于監(jiān)聽 state 變化的 useEffect。通常,我們可以將這些功能放在更新 state 的代碼旁邊。在這里,我們可以將 trackEvent(...) 移動到 toggleDetails 函數(shù)中。
function ProductView({ name, details }) {
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
const toggleDetails = () => {
setIsDetailsVisible(!isDetailsVisible);
trackEvent({ event: "Toggle Product Details", value: !isDetailsVisible });
};
return (
<div>
{name}
<button onClick={toggleDetails}>Show details</button>
{isDetailsVisible && <ProductDetails {...details} />}
</div>
);
}
矛盾的 state
當您使用相互依賴的多個 state 時,這些狀態(tài)可能存在多種組合,稍有不慎就會設置出錯誤的 state,讓這些 state 呈現(xiàn)出相互矛盾的渲染結(jié)果。因此,我們需要更直觀的方式來組織和管理這些狀態(tài)組合。
簡單示例
下面是一個很基本的數(shù)據(jù)請求的示例,組件可以處于不同的狀態(tài):要么正在加載數(shù)據(jù),要么發(fā)生錯誤,要么已成功獲取數(shù)據(jù)。
export function ContradictingState() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setIsLoading(true);
setError(null);
fetchData()
.then((data) => {
setData(data);
setIsLoading(false);
})
.catch((error) => {
setIsLoading(false);
setData(null);
setError(error);
});
}, []);
...
問題分析
這種方法的問題是,如果我們不小心,我們可能會產(chǎn)生有矛盾的 state。例如,在上面的示例中,當發(fā)生錯誤時,我們可能忘記將 isLoading 設置為 false。
對于哪些 state 是允許組合的,也是很難理解的。在上面的例子中,理論上我們可以有 8 種不同的 state 組合。但你不能很直觀的看到哪些狀態(tài)組合是真正存在的。
解決方案
多個狀態(tài)之間相互依賴,更推薦用 useReducer 來替代 useState。
const initialState = {
data: [],
error: null,
isLoading: false
};
function reducer(state, action) {
switch (action.type) {
case "FETCH":
return {
...state,
error: null,
isLoading: true
};
case "SUCCESS":
return {
...state,
error: null,
isLoading: false,
data: action.data
};
case "ERROR":
return {
...state,
isLoading: false,
error: action.error
};
default:
throw new Error(`action "${action.type}" not implemented`);
}
}
export function NonContradictingState() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({ type: "FETCH" });
fetchData()
.then((data) => {
dispatch({ type: "SUCCESS", data });
})
.catch((error) => {
dispatch({ type: "ERROR", error });
});
}, []);
...
這樣一來,就可以大大減少了我們的理解成本。我們可以很直觀地看到我們有 3 個動作和 4 個可能的組件狀態(tài)(“FETCH”、“SUCCESS”、“ERROR”和初始狀態(tài))。
深度嵌套的 state
我們這里提到的最后一個常見問題是(深度)嵌套對象的 state。如果我們只是渲染數(shù)據(jù),這可能不存在什么問題。但是,一旦開始更新嵌套數(shù)據(jù)項,就會遇到一些麻煩。
簡單示例
這里我們有一個組件,用于渲染深度嵌套的注釋。JSX 在這里并不重要,所以省略了,我們假設 updateComment 是綁定到按鈕上的回調(diào)函數(shù)。
function NestedComments() {
const [comments, setComments] = useState([
{
id: "1",
text: "Comment 1",
children: [
{
id: "11",
text: "Comment 1 1"
},
{
id: "12",
text: "Comment 1 2"
}
]
},
{
id: "2",
text: "Comment 2"
},
{
id: "3",
text: "Comment 3",
children: [
{
id: "31",
text: "Comment 3 1",
children: [
{
id: "311",
text: "Comment 3 1 1"
}
]
}
]
}
]);
const updateComment = (id, text) => {
// this gets complicated
};
...
問題分析
這種嵌套 state 的問題是,我們必須以不可變的方式更新它,否則組件不會重新渲染。上面示例中的深度嵌套注釋,我們以硬編碼的方式來實現(xiàn):
const updateComment = (id, text) => {
setComments([
...comments.slice(0, 2),
{
...comments[2],
children: [
{
...comments[2].children[0],
children: [
{
...comments[2].children[0].children[0],
text: "New comment 311"
}
]
}
]
}
]);
};
這種實現(xiàn)方式非常復雜。
解決方案
與深度嵌套的 state 不同,使用扁平的數(shù)據(jù)結(jié)構(gòu)要容易得多。我們可以為每一個數(shù)據(jù)項增加 ID 字段,通過 ID 之間相互引用來描述嵌套關系。代碼看起來像這樣:
function FlatCommentsRoot() {
const [comments, setComments] = useState([
{
id: "1",
text: "Comment 1",
children: ["11", "12"],
},
{
id: "11",
text: "Comment 1 1"
},
{
id: "12",
text: "Comment 1 2"
},
{
id: "2",
text: "Comment 2",
},
{
id: "3",
text: "Comment 3",
children: ["31"],
},
{
id: "31",
text: "Comment 3 1",
children: ["311"]
},
{
id: "311",
text: "Comment 3 1 1"
}
]);
const updateComment = (id, text) => {
const updatedComments = comments.map((comment) => {
if (comment.id !== id) {
return comment;
}
return {
...comment,
text
};
});
setComments(updatedComments);
};
...
現(xiàn)在,通過它的 ID 找到正確的數(shù)據(jù)項,并在數(shù)組中替換它就容易多了。
原文鏈接:https://juejin.cn/post/7179613872855187493
相關推薦
- 2022-07-06 C#數(shù)據(jù)適配器DataAdapter_C#教程
- 2023-10-26 ElementUI日期轉(zhuǎn)為“yyyy-MM-dd“格式
- 2022-07-09 Tensorflow中使用cpu和gpu有什么區(qū)別_python
- 2022-07-22 C語言中字符串詳解
- 2022-07-31 pycharm終端解釋器與Python解釋器配置_python
- 2023-05-22 PyTorch小功能之TensorDataset解讀_python
- 2023-01-14 Python?pandas中to_sql的使用及問題詳解_python
- 2022-07-03 使用pyscript在網(wǎng)頁中撰寫Python程式的方法_python
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學習環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支