使用 需要注意的 5 個問題
使用 需要注意的 5 個問題
開發任何應用程序最具挑戰性的方面通常是管理其狀態。然而,我們經常需要在應用程序中管理多個狀態片段,例如當從外部服務器檢索數據或在應用程序中更新數據時。
狀態管理的困難是今天存在如此多狀態管理庫的原因,而且更多的庫仍在開發中。值得慶幸的是,React 以 hook 的形式提供了幾個用于狀態管理的內置解決方案,這使得 React 中的狀態管理更加容易。
眾所周知,hook 在 React 組件開發中變得越來越重要,特別是在功能組件中,因為它們已經完全取代了對基于類的組件的需求,而基于類的組件是管理有狀態組件的傳統方式。 hook 是 React 中引入的眾多 hook 之一,但是盡管 hook 已經出現幾年了,開發人員仍然容易因為理解不足而犯常見的錯誤。
hook 可能很難理解,特別是對于新手 React 開發人員或從基于類的組件遷移到函數組件的開發人員。在本文中,我們將探討使用 需要注意的 5 個問題,以及如何避免它們。
1. 初始化 錯誤
錯誤地初始化 hook 是開發人員在使用它時最常犯的錯誤之一。問題是 允許你使用任何你想要的東西來定義它的初始狀態。然而,沒有人直接告訴你的是,根據組件在該狀態下的期望,使用錯誤的類型值初始化 可能會導致應用程序中意外的行為,例如無法呈現 UI,導致黑屏錯誤。
例如,我們有一個組件,它期望一個包含用戶名稱、圖像和個人簡歷的用戶對象狀態——在這個組件中,我們呈現用戶的屬性。使用不同的數據類型(如空狀態或空值)初始化 將導致空白頁錯誤,如下所示。
import { useState } from "react";
function App() {
// 初始化狀態
const [user, setUser] = useState();
// 渲染 UI
return (
<div className='App'>
<img src={user.image} alt='profile image' />
<p>User: {user.name}</p>
<p>About: {user.bio}</p>
</div>
);
}
export default App;
我們可以看到頁面一篇空白,檢查控制臺將拋出如下所示的類似錯誤:
新手的開發人員在初始化他們的狀態時經常犯這個錯誤,特別是在從服務器或數據庫獲取數據時,因為檢索到的數據期望用實際的用戶對象更新狀態。然而,這是一種不好的做法,可能會導致預期的行為,如上所示。
初始化 的首選方法是將預期的數據類型傳遞給它,以避免潛在的空白頁錯誤。例如,一個空對象,如下所示,可以傳遞給狀態:
import { useState } from "react";
function App() {
// 使用期望的數據類型初始化狀態
const [user, setUser] = useState({});
// 渲染 UI
return (
<div className='App'>
<img src={user.image} alt='profile image' />
<p>User: {user.name}</p>
<p>About: {user.bio}</p>
</div>
);
}
export default App;
這個時候我們可以看到頁面如下所示:
我們可以更進一步,在初始化狀態時定義用戶對象的預期屬性。
import { useState } from "react";
function App() {
// 使用期望的數據類型初始化狀態
const [user, setUser] = useState({
image: "",
name: "",
bio: "",
});
// 渲染 UI
return (
<div className='App'>

<img src={user.image} alt='profile image' />
<p>User: {user.name}</p>
<p>About: {user.bio}</p>
</div>
);
}
export default App;
2. 沒有使用可選鏈
有時,僅僅使用預期的數據類型初始化 往往不足以防止意外的空白頁錯誤。當試圖訪問深嵌套在相關對象鏈中的深嵌套對象的屬性時,尤其如此。
你通常嘗試通過使用點(.)操作符通過相關對象來訪問該對象,例如 user.names.。但是,如果丟失了任何鏈接的對象或屬性,就會出現問題。頁面將中斷,用戶將得到一個空白頁錯誤。
例如:
import { useState } from "react";
function App() {
// 使用期望的數據類型初始化狀態
const [user, setUser] = useState({});
// 渲染 UI
return (
<div className='App'>
<img src={user.image} alt='profile image' />
<p>User: {user.names.firstName}</p>
<p>About: {user.bio}</p>
</div>
);
}
export default App;
我們可以看到頁面一篇空白,檢查控制臺將拋出如下所示的類似錯誤:
對于這個錯誤和 UI 未呈現的典型解決方案是使用條件檢查來驗證狀態的存在性,在呈現組件之前檢查它是否可訪問,例如 user.names && user.names.,它只在左側表達式為真(如果 user.names 存在)時計算右側表達式。然而,這個解決方案很混亂,因為它需要對每個對象鏈進行多次檢查。
使用可選的鏈接操作符(?.),你可以讀取深埋在相關對象鏈中的屬性值,而不需要驗證每個引用的對象是否有效。可選的鏈接操作符(?.)就像點鏈接操作符(.),不同的是,如果引用的對象或屬性缺失(即 null 或 ),表達式短路并返回 值。簡單地說,如果丟失了任何鏈接對象,它就不會繼續進行鏈接操作(短路)。
例如,user?.names?. 不會拋出任何錯誤或中斷頁面,因為一旦它檢測到 user 或 names 對象丟失,它將立即終止操作。
import { useState } from "react";
function App() {
// 使用期望的數據類型初始化狀態
const [user, setUser] = useState({});
// 渲染 UI
return (
<div className='App'>
<img src={user.image} alt='profile image' />
<p>User: {user?.names?.firstName}</p>
<p>About: {user.bio}</p>
</div>
);
}
export default App;
在訪問狀態中的鏈接屬性時,利用可選的鏈接操作符可以簡化和縮短表達式,這在探索對象的內容時非常有用,對象的引用可能事先不知道。
3. 直接更新
缺乏對 React 如何調度和更新狀態的正確理解,很容易導致在更新應用程序狀態時出現錯誤。在使用 時,我們通常定義一個狀態并使用 set state 函數直接更新狀態。
例如,我們創建了一個計數狀態和一個附加到按鈕的 函數,該函數在單擊時為狀態添加 1(+1):
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);

// 直接更新狀態
const increase = () => setCount(count + 1);
// 渲染 UI
return (
<div className='App'>
<span>Count: {count}</span>
<button onClick={increase}>Add +1</button>
</div>
);
}
export default App;
這和預期的一樣。但是,直接更新狀態是一種不好的做法,在處理多個用戶使用的實時應用程序時可能會導致潛在的錯誤。為什么?因為與你所想的相反,React 不會在單擊按鈕時立即更新狀態。相反,React 獲取當前狀態的快照,并將更新(+1)安排在稍后執行,以獲得性能提升——這發生在幾毫秒內,因此肉眼不會注意到。然而,雖然預定的更新仍然處于暫掛的轉換中,但當前狀態可能會被其他內容更改(例如多個用戶的情況)。預定的更新將無法知道這個新事件,因為它只有單擊按鈕時所獲得的狀態快照的記錄。
這可能會導致應用程序出現嚴重的錯誤和奇怪的行為。讓我們通過添加另一個按鈕來查看實際操作,該按鈕在延遲 2 秒后異步更新計數狀態。
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
// 直接更新狀態
const update = () => setCount(count + 1);
// 在 2 秒后直接更新狀態
const asyncUpdate = () => {
setTimeout(() => {
setCount(count + 1);
}, 2000);
};
// 渲染 UI
return (
<div className='App'>
<span>Count: {count}</span>
<button onClick={update}>Add +1</button>
<button onClick={asyncUpdate}>Add +1 later</button>
</div>
);
}
請注意輸出中的錯誤:
注意到這個錯誤嗎?我們首先兩次點擊第一個“Add +1”按鈕(這將更新狀態為1 +1 = 2),之后,我們點擊“Add +1 later” —— 這將獲取當前狀態(2)的快照,并在兩秒后調度更新,向該狀態添加 1。但是當這個計劃更新還處于過渡階段時,我們繼續點擊“Add +1”按鈕三次,將當前狀態更新為5(即2 +1 +1 +1 = 5)。然而,異步定時更新嘗試在兩秒鐘后使用它在內存中的快照(2)更新狀態)即 2 + 1 = 3),而沒有意識到當前狀態已更新為 5。結果,狀態被更新為 3 而不是 6。
這個無意的錯誤經常困擾那些僅使用 () 函數直接更新狀態的應用程序。更新 的建議方法是通過函數更新來傳遞給 () 一個回調函數,在這個回調函數中我們傳遞該實例的當前狀態,例如 ( => + )。這將在預定的更新時間將當前狀態傳遞給回調函數,從而可以在嘗試更新之前知道當前狀態。
因此,讓我們修改示例演示,使用函數更新而不是直接更新。
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
// 直接更新狀態
const update = () => setCount(count + 1);
// 在 2 秒后直接更新狀態
const asyncUpdate = () => {
setTimeout(() => {
setCount((currentCount) => currentCount + 1);
}, 2000);
};
// 渲染 UI

return (
<div className='App'>
<span>Count: {count}</span>
<button onClick={update}>Add +1</button>
<button onClick={asyncUpdate}>Add +1 later</button>
</div>
);
}
export default App;
通過函數式更新,() 函數知道狀態已更新為 5,因此它將狀態更新為 6。
4. 更新特定對象屬性
另一個常見錯誤是只修改對象或數組的屬性而不修改引用本身。
例如,我們用定義好的 name 和 age 屬性初始化一個用戶對象。然而,我們的組件有一個按鈕,它試圖只更新用戶名,如下所示。
import { useState, useEffect } from "react";
export default function App() {
const [user, setUser] = useState({
name: "John",
age: 25,
});
// 更新用戶狀態屬性
const changeName = () => {
setUser((user) => (user.name = "Mark"));
};
// 渲染 UI
return (
<div className='App'>
<p>User: {user.name}</p>
<p>Age: {user.age}</p>
<button onClick={changeName}>Change name</button>
</div>
);
}
點擊按鈕前的初始狀態:
點擊按鈕后的更新狀態:
正如你所看到的,用戶不再是一個對象,而是被改寫為字符串 "Mark",而不是特定的屬性被修改。為什么?因為 () 將返回或傳遞給它的任何值賦值為新狀態。
一種典型的老式方法是創建一個新的對象引用品牌輸入屬性錯誤,并將前一個用戶對象分配給它,直接修改用戶名。
import { useState, useEffect } from "react";
export default function App() {
const [user, setUser] = useState({
name: "John",
age: 25,
});
// 更新用戶狀態屬性
const changeName = () => {
setUser((user) => Object.assign({}, user, { name: "Mark" }));
};
// 渲染 UI
return (
<div className='App'>
<p>User: {user.name}</p>

<p>Age: {user.age}</p>
<button onClick={changeName}>Change name</button>
</div>
);
}
點擊按鈕后的更新狀態:
注意,只有用戶名被修改了,而其他屬性保持不變。
然而,更新特定屬性、對象或數組的理想而現代的方法是使用 ES6 擴展操作符(...)。在處理功能組件中的狀態時,這是更新對象或數組的特定屬性的理想方法。使用這個擴展操作符,你可以輕松地將現有項的屬性解包到新項中,同時修改或向解包項添加新屬性。
import { useState, useEffect } from "react";
export default function App() {
const [user, setUser] = useState({
name: "John",
age: 25,
});
// 使用擴展操作符更新用戶狀態的屬性
const changeName = () => {
setUser((user) => ({ ...user, name: "Mark" }));
};
// 渲染 UI
return (
<div className='App'>
<p>User: {user.name}</p>
<p>Age: {user.age}</p>
<button onClick={changeName}>Change name</button>
</div>
);
}
結果將與上一個狀態相同。單擊按鈕后,name 屬性將被更新,而其他用戶屬性保持不變。
5. 管理表單中的多個輸入字段
管理表單中的幾個受控輸入通常是通過為每個輸入字段手動創建多個 () 函數并將每個函數綁定到相應的輸入字段來完成的。例如:
import { useState, useEffect } from "react";
export default function App() {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [age, setAge] = useState("");
const [userName, setUserName] = useState("");
const [password, setPassword] = useState("");
const [email, setEmail] = useState("");
// 渲染 UI
return (
<div className='App'>
<form>
<input type='text' placeholder='First Name' />
<input type='text' placeholder='Last Name' />
<input type='number' placeholder='Age' />
<input type='text' placeholder='Username' />
<input type='password' placeholder='Password' />
<input type='email' placeholder='email' />
</form>
</div>
);
}
此外,必須為每個輸入創建處理程序函數,以建立雙向數據流,在輸入值輸入時更新每個狀態。這可能是相當多余和耗時的,因為它涉及編寫大量代碼,降低了代碼庫的可讀性。
但是,只使用一個 hook 就可以管理表單中的多個輸入字段。這可以通過以下方法實現:首先為每個輸入字段指定一個惟一的名稱,然后創建一個 () 函數,該函數使用與輸入字段名稱相同的屬性進行初始化:
import { useState, useEffect } from "react";
export default function App() {
const [user, setUser] = useState({
firstName: "",
lastName: "",
age: "",
username: "",
password: "",
email: "",
});
// 渲染 UI
return (
<div className='App'>
<form>
<input type='text' name='firstName' placeholder='First Name' />
<input type='text' name='lastName' placeholder='Last Name' />
<input type='number' name='age' placeholder='Age' />
<input type='text' name='username' placeholder='Username' />
<input type='password' name='password' placeholder='Password' />
<input type='email' name='email' placeholder='email' />
</form>
</div>
);
}
在此之后,我們創建一個處理程序事件函數,該函數更新用戶對象的特定屬性,以反映每當用戶輸入內容時表單中的更改。這可以通過使用拓展操作符和使用 event.. = event..value 動態訪問觸發處理程序函數的特定輸入元素的名稱來完成。
換句話說,我們通常檢查傳遞給事件函數的事件對象,獲取目標元素名稱(與用戶狀態下的屬性名稱相同),并用目標元素中的關聯值更新它品牌輸入屬性錯誤,如下所示:
import { useState, useEffect } from "react";
export default function App() {
const [user, setUser] = useState({
firstName: "",
lastName: "",
age: "",
username: "",
password: "",
email: "",
});
// 更新特定的輸入字段
const handleChange = (e) =>
setUser(prevState => ({...prevState, [e.target.name]: e.target.value}))
// 渲染 UI
return (
<div className='App'>
<form>
<input type='text' onChange={handleChange} name='firstName' placeholder='First Name' />
<input type='text' onChange={handleChange} name='lastName' placeholder='Last Name' />
<input type='number' onChange={handleChange} name='age' placeholder='Age' />
<input type='text' onChange={handleChange} name='username' placeholder='Username' />
<input type='password' onChange={handleChange} name='password' placeholder='Password' />
<input type='email' onChange={handleChange} name='email' placeholder='email' />
</form>
</div>
);
}
在此實現中,對于每個用戶輸入都觸發事件處理程序函數。在這個事件函數中,我們有一個 () 狀態函數,它接受用戶的以前/當前狀態,并使用拓展操作符解包這個用戶狀態。然后檢查事件對象中觸發函數的目標元素名(與狀態中的屬性名相關)。獲得此屬性名后,我們修改它以反映表單中的用戶輸入值。
6. 小結
作為一個創建高度交互用戶界面的 React 開發人員,你可能犯過上面提到的一些錯誤。希望這些有用的 實踐能夠幫助你在構建 React 驅動的應用程序時使用 hook 避免這些潛在的錯誤。