useState
是一個 React Hook,可讓你將狀態變數新增到你的元件中。
const [state, setState] = useState(initialState)
參考
useState(initialState)
在元件的頂層呼叫 useState
來宣告狀態變數。
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(28);
const [name, setName] = useState('Taylor');
const [todos, setTodos] = useState(() => createTodos());
// ...
慣例是使用 陣列解構 將狀態變數命名為 [something, setSomething]
。
參數
initialState
:你希望狀態初始的值。它可以是任何類型的值,但函式有特殊的行為。初始渲染後,此參數將被忽略。- 如果你傳遞一個函式作為
initialState
,它將被視為*初始化器函式*。它應該是純粹的,不應接受任何參數,並且應返回任何類型的值。 React 將在初始化元件時呼叫你的初始化器函式,並將其返回值儲存為初始狀態。請參考以下範例。
- 如果你傳遞一個函式作為
回傳值
useState
會返回一個剛好包含兩個值的陣列
- 目前的狀態。在第一次渲染期間,它將與你傳遞的
initialState
相符。 - 允許你將狀態更新為不同值並觸發重新渲染的
set
函式。
注意事項
useState
是一個 Hook,所以你只能在組件的頂層或你自己的 Hooks 中呼叫它。你不能在迴圈或條件式中呼叫它。如果你需要這樣做,請提取一個新的組件並將狀態移入其中。- 在嚴格模式下,React 會呼叫你的初始化函式兩次,以便幫助你找到意外的非純粹性。這僅在開發環境中發生,不會影響生產環境。如果你的初始化函式是純粹的(它 seharusnya如此),這不應該影響行為。其中一次呼叫的結果將被忽略。
像 setSomething(nextState)
這樣的 set
函式,是由 useState
返回,讓你更新狀態到不同的值並觸發重新渲染。你可以直接傳遞下一個狀態,或者傳遞一個根據前一個狀態計算它的函式。
由 useState
返回的 set
函式允許你將狀態更新為不同的值並觸發重新渲染。你可以直接傳遞下一個狀態值,或者傳遞一個函式,該函式會根據前一個狀態計算出下一個狀態值。
const [name, setName] = useState('Edward');
function handleClick() {
setName('Taylor');
setAge(a => a + 1);
// ...
參數
nextState
:你希望狀態成為的值。它可以是任何類型的值,但對於函式來說有一個特殊的行為。- 如果你傳遞一個函式作為
nextState
,它將被視為一個*更新函式*。它必須是純粹的,應該將待處理的狀態作為其唯一參數,並且應該返回下一個狀態。React 將你的更新函式放入佇列並重新渲染你的組件。在下一次渲染期間,React 將通過將所有佇列中的更新程式應用於先前的狀態來計算下一個狀態。請參閱下面的範例。
- 如果你傳遞一個函式作為
回傳值
set
函式沒有回傳值。
注意事項
-
set
函式只會更新下一次渲染的狀態變數。如果你在呼叫set
函式後讀取狀態變數,你仍然會得到在你呼叫之前的舊值。 -
如果新值與目前的
state
相同(透過Object.is
比較判斷),React 將會**略過重新渲染組件及其子組件**。這是一種效能優化。雖然在某些情況下,React 可能仍然需要在略過子組件之前呼叫你的組件,但这不應該影響你的程式碼。 -
React 會批次處理狀態更新。它會在所有事件處理程式都執行完畢並呼叫了它們的
set
函式之後更新螢幕。這可以防止在單一事件中多次重新渲染。在極少數情況下,你需要強制 React 更早更新螢幕(例如為了存取 DOM),你可以使用flushSync
。 -
set
函式具有穩定的識別碼,因此你經常會看到它從 Effect 的相依性中被省略,但包含它並不會導致 Effect 觸發。如果程式碼檢查工具允許你在沒有錯誤的情況下省略相依性,則可以安全地這樣做。瞭解更多關於移除 Effect 相依性的資訊。 -
僅允許在當前正在渲染的組件內*渲染期間*呼叫
set
函式。React 將會捨棄其輸出,並立即嘗試使用新的狀態再次渲染它。這種模式很少需要,但你可以使用它來**儲存先前渲染的資訊**。請參閱下面的範例。 -
在嚴格模式下,React 會**呼叫你的更新函式兩次**,以便幫助你找到意外的非純粹性。這僅在開發環境中發生,不會影響生產環境。如果你的更新函式是純粹的(它 seharusnya如此),這不應該影響行為。其中一次呼叫的結果將被忽略。
用法
將狀態添加到組件
在組件的頂層呼叫 useState
來宣告一個或多個 狀態變數。
import { useState } from 'react';
function MyComponent() {
const [age, setAge] = useState(42);
const [name, setName] = useState('Taylor');
// ...
慣例是使用 陣列解構 將狀態變數命名為 [something, setSomething]
。
useState
會回傳一個剛好包含兩個項目的陣列
- 這個狀態變數的目前狀態,初始設定為您提供的初始狀態。
set
函式,讓您可以因應互動將其更改為任何其他值。
要更新螢幕上的內容,請使用一些新的狀態值來呼叫 set
函式
function handleClick() {
setName('Robin');
}
React 會儲存新的狀態,使用新的值重新渲染您的組件,並更新 UI。
根據先前狀態更新狀態
假設 age
是 42
。這個處理函式會呼叫 setAge(age + 1)
三次
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
但是,點擊一次後,age
只會是 43
而不是 45
!這是因為呼叫 set
函式 並不會更新 正在執行的程式碼中的 age
狀態變數。所以每次呼叫 setAge(age + 1)
都會變成 setAge(43)
。
要解決這個問題,您可以**傳遞一個*更新函式***給 setAge
,而不是下一個狀態值
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
這裡,a => a + 1
是您的更新函式。它會取得待處理的狀態並據此計算下一個狀態。
React 會將您的更新函式放入一個佇列中。然後,在下一次渲染期間,它會以相同的順序呼叫它們
a => a + 1
將會收到42
作為待處理的狀態,並回傳43
作為下一個狀態。a => a + 1
將會收到43
作為待處理的狀態,並回傳44
作為下一個狀態。a => a + 1
將會收到44
作為待處理的狀態,並回傳45
作為下一個狀態。
沒有其他排隊的更新,因此 React 最終會將 45
儲存為目前狀態。
按照慣例,通常會將待處理狀態參數命名為狀態變數名稱的第一個字母,例如 age
的 a
。但是,您也可以將其稱為 prevAge
或您覺得更清楚的其他名稱。
在開發過程中,React 可能會呼叫您的更新函式兩次,以驗證它們是純粹的。
深入探討
您可能會聽到一個建議,如果要設定的狀態是根據先前狀態計算的,則始終編寫像 setAge(a => a + 1)
這樣的程式碼。這樣做沒有壞處,但也不一定總是必要的。
在大多數情況下,這兩種方法沒有區別。React始終確保針對刻意的使用者操作(例如點擊),在下次點擊之前會先更新 age
狀態變數。這表示點擊處理函式在事件處理函式開始時沒有看到「過時」的 age
的風險。
但是,如果您在同一個事件中進行多次更新,更新函式可能會有幫助。如果存取狀態變數本身不方便,它們也很有幫助(在最佳化重新渲染時可能會遇到這種情況)。
如果您更重視一致性,而不是稍微冗長的語法,那麼在設定的狀態是根據先前狀態計算得出時,始終編寫一個更新器是合理的。如果它是根據某些*其他*狀態變數的先前狀態計算得出的,您可能需要將它們組合成一個物件,並使用 Reducer。
計數器(數字) 1範例 2: 傳遞更新器函式
此範例傳遞更新器函式,因此「+3」按鈕可以正常運作。
import { useState } from 'react'; export default function Counter() { const [age, setAge] = useState(42); function increment() { setAge(a => a + 1); } return ( <> <h1>Your age: {age}</h1> <button onClick={() => { increment(); increment(); increment(); }}>+3</button> <button onClick={() => { increment(); }}>+1</button> </> ); }
更新狀態中的物件和陣列
您可以將物件和陣列放入狀態中。在 React 中,狀態被視為唯讀的,因此您應該*取代*它而不是*變更*您現有的物件。例如,如果您在狀態中有一個 form
物件,請不要變更它
// 🚩 Don't mutate an object in state like this:
form.firstName = 'Taylor';
而是透過建立一個新的物件來取代整個物件
// ✅ Replace state with a new object
setForm({
...form,
firstName: 'Taylor'
});
計數器(數字) 1範例 4: 表單 (物件)
在此範例中,form
狀態變數儲存一個物件。每個輸入都有一個變更處理程式,它會使用整個表單的下一個狀態呼叫 setForm
。{ ...form }
展開語法可確保取代狀態物件而不是變更它。
import { useState } from 'react'; export default function Form() { const [form, setForm] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', }); return ( <> <label> First name: <input value={form.firstName} onChange={e => { setForm({ ...form, firstName: e.target.value }); }} /> </label> <label> Last name: <input value={form.lastName} onChange={e => { setForm({ ...form, lastName: e.target.value }); }} /> </label> <label> Email: <input value={form.email} onChange={e => { setForm({ ...form, email: e.target.value }); }} /> </label> <p> {form.firstName}{' '} {form.lastName}{' '} ({form.email}) </p> </> ); }
避免重新建立初始狀態
React 會儲存初始狀態一次,並在下次渲染時忽略它。
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...
雖然 createInitialTodos()
的結果僅用於初始渲染,但您仍在每次渲染時呼叫此函式。如果它正在建立大型陣列或執行昂貴的計算,這可能會很浪費。
要解決此問題,您可以將它作為*初始化器*函式傳遞給 useState
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
// ...
請注意,您傳遞的是 createInitialTodos
,它是*函式本身*,而不是 createInitialTodos()
,它是呼叫它的結果。如果您將函式傳遞給 useState
,React 將僅在初始化期間呼叫它。
React 可能會在開發中呼叫您的初始化器兩次,以驗證它們是純粹的。
計數器(數字) 1範例 2: 傳遞初始化器函式
這個例子傳遞了初始化函數,因此 `createInitialTodos` 函數只會在初始化期間運行。它不會在組件重新渲染時運行,例如在您輸入到輸入框時。
import { useState } from 'react'; function createInitialTodos() { const initialTodos = []; for (let i = 0; i < 50; i++) { initialTodos.push({ id: i, text: 'Item ' + (i + 1) }); } return initialTodos; } export default function TodoList() { const [todos, setTodos] = useState(createInitialTodos); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => { setText(''); setTodos([{ id: todos.length, text: text }, ...todos]); }}>Add</button> <ul> {todos.map(item => ( <li key={item.id}> {item.text} </li> ))} </ul> </> ); }
使用 key 重設狀態
當您渲染列表時,您經常會遇到 `key` 屬性。然而,它還有另一個用途。
您可以**通過將不同的 `key` 傳遞給組件來重設組件的狀態。** 在這個例子中,「重設」按鈕會更改 `version` 狀態變數,我們將其作為 `key` 傳遞給 `Form`。當 `key` 更改時,React 會從頭開始重新創建 `Form` 組件(及其所有子組件),因此其狀態會被重設。
閱讀保留和重設狀態以了解更多資訊。
import { useState } from 'react'; export default function App() { const [version, setVersion] = useState(0); function handleReset() { setVersion(version + 1); } return ( <> <button onClick={handleReset}>Reset</button> <Form key={version} /> </> ); } function Form() { const [name, setName] = useState('Taylor'); return ( <> <input value={name} onChange={e => setName(e.target.value)} /> <p>Hello, {name}.</p> </> ); }
儲存先前渲染的資訊
通常,您會在事件處理程式中更新狀態。然而,在極少數情況下,您可能希望響應渲染來調整狀態——例如,您可能希望在 prop 改變時更改狀態變數。
在大多數情況下,您不需要這樣做
- **如果您可以完全根據目前的 props 或其他狀態計算出所需的值,請完全移除該冗餘狀態。** 如果您擔心重新計算太頻繁,`useMemo` Hook 可以提供幫助。
- 如果您想重設整個組件樹的狀態,請將不同的 `key` 傳遞給您的組件。
- 如果可以,請在事件處理程式中更新所有相關狀態。
在極少數情況下,如果以上皆不適用,您可以使用一種模式,通過在組件渲染時調用 `set` 函數,根據已渲染的值來更新狀態。
以下是一個例子。 `CountLabel` 組件顯示傳遞給它的 `count` prop
export default function CountLabel({ count }) {
return <h1>{count}</h1>
}
假設您想顯示計數器自上次更改以來是*增加還是減少*。 `count` prop 並沒有告訴您這一點——您需要追蹤它的先前值。新增 `prevCount` 狀態變數來追蹤它。新增另一個名為 `trend` 的狀態變數來保存計數是增加還是減少。比較 `prevCount` 與 `count`,如果它們不相等,則更新 `prevCount` 和 `trend`。現在您可以顯示目前的 count prop 以及*它自上次渲染以來如何變化*。
import { useState } from 'react'; export default function CountLabel({ count }) { const [prevCount, setPrevCount] = useState(count); const [trend, setTrend] = useState(null); if (prevCount !== count) { setPrevCount(count); setTrend(count > prevCount ? 'increasing' : 'decreasing'); } return ( <> <h1>{count}</h1> {trend && <p>The count is {trend}</p>} </> ); }
請注意,如果您在渲染時調用 `set` 函數,它必須在像 `prevCount !== count` 這樣的條件內,並且在條件內必須有像 `setPrevCount(count)` 這樣的調用。否則,您的組件會在迴圈中重新渲染,直到崩潰。此外,您只能像這樣更新*當前正在渲染*的組件的狀態。在渲染期間調用*另一個*組件的 `set` 函數是一個錯誤。最後,您的 `set` 調用仍然應該在不發生突變的情況下更新狀態——這並不意味著您可以違反純函數的其他規則。
這種模式可能難以理解,通常最好避免。然而,它比在 effect 中更新狀態更好。當您在渲染期間調用 `set` 函數時,React 將在您的組件使用 `return` 陳述式退出後,以及在渲染子組件之前立即重新渲染該組件。這樣,子組件就不需要渲染兩次。您的組件函數的其餘部分仍然會執行(結果將被丟棄)。如果您的條件位於所有 Hook 調用下方,您可以新增一個提前的 `return;` 來更快地重新開始渲染。
疑難排解
我已更新狀態,但記錄顯示的是舊值
調用 `set` 函數**不會更改正在運行的程式碼中的狀態**
function handleClick() {
console.log(count); // 0
setCount(count + 1); // Request a re-render with 1
console.log(count); // Still 0!
setTimeout(() => {
console.log(count); // Also 0!
}, 5000);
}
這是因為狀態的行為類似於快照。更新狀態會請求使用新的狀態值進行另一次渲染,但不會影響已在運行的事件處理程式中的 `count` JavaScript 變數。
如果您需要使用下一個狀態,您可以在將其傳遞給 `set` 函數之前將其保存在變數中
const nextCount = count + 1;
setCount(nextCount);
console.log(count); // 0
console.log(nextCount); // 1
我已經更新了狀態,但畫面沒有更新
如果下一個狀態與前一個狀態相同,React 將**忽略您的更新**,這由 Object.is
比較決定。這通常發生在您直接更改狀態中的物件或陣列時。
obj.x = 10; // 🚩 Wrong: mutating existing object
setObj(obj); // 🚩 Doesn't do anything
您變更了現有的 obj
物件並將其傳回給 setObj
,因此 React 忽略了更新。要解決此問題,您需要確保始終**取代**狀態中的物件和陣列,而不是**變更**它們。
// ✅ Correct: creating a new object
setObj({
...obj,
x: 10
});
我收到一個錯誤:「太多次重新渲染」
您可能會收到一條錯誤訊息:太多次重新渲染。React 限制渲染次數以防止無限迴圈。
通常,這表示您在**渲染期間**無條件地設定狀態,因此您的組件進入了一個迴圈:渲染、設定狀態(導致渲染)、渲染、設定狀態(導致渲染),依此類推。很多時候,這是由於指定事件處理程式時出錯所致。
// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>
// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>
// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>
如果您找不到此錯誤的原因,請點擊主控台中錯誤旁邊的箭頭,並瀏覽 JavaScript 堆疊以找到導致錯誤的特定 set
函式呼叫。
我的初始化或更新函式會執行兩次
在**嚴格模式**下,React 會將您的某些函式呼叫兩次而不是一次。
function TodoList() {
// This component function will run twice for every render.
const [todos, setTodos] = useState(() => {
// This initializer function will run twice during initialization.
return createTodos();
});
function handleClick() {
setTodos(prevTodos => {
// This updater function will run twice for every click.
return [...prevTodos, createTodo()];
});
}
// ...
這是預期的行為,不應該會破壞您的程式碼。
這種**僅限開發環境**的行為可以幫助您**保持組件的純粹性**。 React 使用其中一次呼叫的結果,並忽略另一次呼叫的結果。只要您的組件、初始化函式和更新函式是純粹的,這就不會影響您的邏輯。但是,如果它們意外地不純粹,這可以幫助您注意到錯誤。
例如,這個不純粹的更新函式會改變狀態中的一個陣列:
setTodos(prevTodos => {
// 🚩 Mistake: mutating state
prevTodos.push(createTodo());
});
因為 React 會呼叫您的更新函式兩次,您會看到待辦事項被添加了兩次,因此您會知道有一個錯誤。在此範例中,您可以透過**取代陣列而不是改變它**來修正錯誤。
setTodos(prevTodos => {
// ✅ Correct: replacing with new state
return [...prevTodos, createTodo()];
});
現在這個更新函式是純粹的,額外呼叫它一次並不會改變行為。這就是為什麼 React 呼叫它兩次可以幫助您找到錯誤的原因。**只有組件、初始化函式和更新函式需要是純粹的。** 事件處理程式不需要是純粹的,因此 React 永遠不會呼叫您的事件處理程式兩次。
我正在嘗試將狀態設定為一個函式,但它卻被呼叫了
您不能像這樣將函式放入狀態中:
const [fn, setFn] = useState(someFunction);
function handleClick() {
setFn(someOtherFunction);
}
因為您傳遞的是一個函式,React 假設 someFunction
是一個**初始化函式**,而 someOtherFunction
是一個**更新函式** ,所以它會嘗試呼叫它們並儲存結果。要實際**儲存**一個函式,您必須在兩種情況下都在它們前面加上 () =>
。然後 React 就會儲存您傳遞的函式。
const [fn, setFn] = useState(() => someFunction);
function handleClick() {
setFn(() => someOtherFunction);
}