useReducer

useReducer 是一個 React Hook,可讓你將 reducer 新增至你的元件。

const [state, dispatch] = useReducer(reducer, initialArg, init?)

參考

useReducer(reducer, initialArg, init?)

在元件的最上層呼叫 useReducer,使用 reducer 管理其狀態。

import { useReducer } from 'react';

function reducer(state, action) {
// ...
}

function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...

請參閱以下更多範例。

參數

  • reducer:指定狀態如何更新的 reducer 函式。它必須是純粹的,應將狀態和 action 作為參數,並應傳回下一個狀態。狀態和 action 可以是任何類型。
  • initialArg:用於計算初始狀態的值。它可以是任何類型的值。初始狀態如何從它計算出來取決於下一個 init 參數。
  • 選用 init:應傳回初始狀態的初始化函式。如果未指定,則初始狀態設為 initialArg。否則,初始狀態設為呼叫 init(initialArg) 的結果。

傳回值

useReducer 傳回一個恰好包含兩個值的陣列

  1. 目前的狀態。在第一次渲染期間,它設為 init(initialArg)initialArg(如果沒有 init)。
  2. 允許你將狀態更新為不同值並觸發重新渲染的 dispatch 函式

注意事項

  • useReducer 是一個 Hook,所以你只能在組件的頂層或你自己的 Hooks 中調用它。你不能在迴圈或條件語句中調用它。如果你需要這樣做,請提取一個新的組件並將狀態移入其中。
  • dispatch 函式具有穩定的識別性,因此你經常會看到它從 Effect 相依性中被省略,但包含它不會導致 Effect 觸發。如果程式碼檢查器允許你在沒有錯誤的情況下省略一個相依性,那麼這樣做是安全的。深入了解移除 Effect 相依性。
  • 在嚴格模式下,React 會調用你的 reducer 和初始值設定項兩次,以便幫助你找到意外的非純粹函式。 這只是開發階段的行為,不會影響生產環境。如果你的 reducer 和初始值設定項是純粹的(它們應該如此),這不應該影響你的邏輯。其中一次調用的結果會被忽略。

dispatch 函式

useReducer 返回的 dispatch 函式允許你將狀態更新為不同的值並觸發重新渲染。你需要將 action 作為唯一參數傳遞給 dispatch 函式。

const [state, dispatch] = useReducer(reducer, { age: 42 });

function handleClick() {
dispatch({ type: 'incremented_age' });
// ...

React 將通過使用你提供的目前的 state 和你傳遞給 dispatch 的 action 調用 reducer 函式的結果來設定下一個狀態。

參數

  • action:使用者執行的動作。它可以是任何類型的值。按照慣例,action 通常是一個物件,帶有 type 屬性來識別它,以及可選擇的其他屬性來提供額外資訊。

回傳值

dispatch 函式沒有回傳值。

注意事項

  • dispatch 函式只會更新下一次渲染的狀態變數。如果你在調用 dispatch 函式後讀取狀態變數,你仍然會得到在你調用之前的螢幕上的舊值

  • 如果根據 Object.is 比較,你提供的新值與目前的 state 相同,React 將會略過重新渲染組件及其子組件。 這是一種優化。React 在忽略結果之前可能仍然需要調用你的組件,但这不應該影響你的程式碼。

  • React 會批次處理狀態更新。它會在所有事件處理程式都運行完畢並調用它們的 set 函式之後更新螢幕。這可以防止在單一事件中多次重新渲染。在極少數情況下,你需要強制 React 更早更新螢幕,例如訪問 DOM,你可以使用 flushSync


用法

將 reducer 新增至組件

在組件的頂層調用 useReducer ,使用 reducer 管理狀態。

import { useReducer } from 'react';

function reducer(state, action) {
// ...
}

function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...

useReducer 會回傳一個剛好包含兩個項目的陣列。

  1. 這個狀態變數的目前狀態,初始設定為您提供的初始狀態
  2. 讓您可以因應互動來改變狀態的dispatch 函式

要更新螢幕上的內容,請使用一個代表使用者動作的物件(稱為 _動作_)來呼叫 dispatch

function handleClick() {
dispatch({ type: 'incremented_age' });
}

React 會將目前的狀態和動作傳遞給您的reducer 函式。您的 reducer 將計算並返回下一個狀態。React 會儲存該下一個狀態,使用它來渲染您的元件,並更新 UI。

import { useReducer } from 'react';

function reducer(state, action) {
  if (action.type === 'incremented_age') {
    return {
      age: state.age + 1
    };
  }
  throw Error('Unknown action.');
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });

  return (
    <>
      <button onClick={() => {
        dispatch({ type: 'incremented_age' })
      }}>
        Increment age
      </button>
      <p>Hello! You are {state.age}.</p>
    </>
  );
}

useReduceruseState 非常相似,但它允許您將狀態更新邏輯從事件處理程式移到元件外部的單個函式中。閱讀更多關於選擇 useStateuseReducer 的說明。


撰寫 reducer 函式

reducer 函式的宣告如下:

function reducer(state, action) {
// ...
}

接著,您需要填寫將計算並返回下一個狀態的程式碼。按照慣例,通常將它寫成 switch 陳述式。對於 switch 中的每個 case,計算並返回一些下一個狀態。

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}

動作可以有任何形狀。按照慣例,通常會傳遞具有 type 屬性的物件,用於識別動作。它應該包含 reducer 計算下一個狀態所需的最少必要資訊。

function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });

function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}

function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
// ...

動作類型名稱是您的元件的區域性名稱。每個動作都描述單個互動,即使該互動導致資料的多個變更。狀態的形狀是任意的,但通常它是一個物件或陣列。

閱讀將狀態邏輯提取到 reducer 中以了解更多資訊。

陷阱

狀態是唯讀的。不要修改狀態中的任何物件或陣列。

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Don't mutate an object in state like this:
state.age = state.age + 1;
return state;
}

相反,始終從 reducer 返回新的物件。

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Instead, return a new object
return {
...state,
age: state.age + 1
};
}

閱讀更新狀態中的物件更新狀態中的陣列以了解更多資訊。

基本的 useReducer 範例

範例 1 3:
表單(物件)

在此範例中,reducer 管理一個具有兩個欄位的狀態物件:nameage

import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}

const initialState = { name: 'Taylor', age: 42 };

export default function Form() {
  const [state, dispatch] = useReducer(reducer, initialState);

  function handleButtonClick() {
    dispatch({ type: 'incremented_age' });
  }

  function handleInputChange(e) {
    dispatch({
      type: 'changed_name',
      nextName: e.target.value
    }); 
  }

  return (
    <>
      <input
        value={state.name}
        onChange={handleInputChange}
      />
      <button onClick={handleButtonClick}>
        Increment age
      </button>
      <p>Hello, {state.name}. You are {state.age}.</p>
    </>
  );
}


避免重新建立初始狀態

React 會儲存初始狀態一次,並在下次渲染時忽略它。

function createInitialState(username) {
// ...
}

function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...

雖然 createInitialState(username) 的結果僅用於初始渲染,但您仍在每次渲染時呼叫此函式。如果它正在建立大型陣列或執行昂貴的計算,這可能會很浪費。

要解決此問題,您可以將其作為 _初始化器_ 函式傳遞給 useReducer 作為第三個參數

function createInitialState(username) {
// ...
}

function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...

請注意,您傳遞的是 createInitialState(即 _函式本身_),而不是 createInitialState()(即呼叫它的結果)。這樣,初始狀態就不會在初始化後重新建立。

在上面的範例中,createInitialState 接受一個 username 參數。如果您的初始化器不需要任何資訊來計算初始狀態,您可以將 null 作為第二個參數傳遞給 useReducer

傳遞初始化器 (Initializer) 與直接傳遞初始狀態 (Initial State) 的差異

範例 1 2:
傳遞初始化器函式

此範例傳遞了初始化器函式,因此 createInitialState 函式僅在初始化期間執行。當元件重新渲染時(例如,在輸入框中輸入內容時),它不會執行。

import { useReducer } from 'react';

function createInitialState(username) {
  const initialTodos = [];
  for (let i = 0; i < 50; i++) {
    initialTodos.push({
      id: i,
      text: username + "'s task #" + (i + 1)
    });
  }
  return {
    draft: '',
    todos: initialTodos,
  };
}

function reducer(state, action) {
  switch (action.type) {
    case 'changed_draft': {
      return {
        draft: action.nextDraft,
        todos: state.todos,
      };
    };
    case 'added_todo': {
      return {
        draft: '',
        todos: [{
          id: state.todos.length,
          text: state.draft
        }, ...state.todos]
      }
    }
  }
  throw Error('Unknown action: ' + action.type);
}

export default function TodoList({ username }) {
  const [state, dispatch] = useReducer(
    reducer,
    username,
    createInitialState
  );
  return (
    <>
      <input
        value={state.draft}
        onChange={e => {
          dispatch({
            type: 'changed_draft',
            nextDraft: e.target.value
          })
        }}
      />
      <button onClick={() => {
        dispatch({ type: 'added_todo' });
      }}>Add</button>
      <ul>
        {state.todos.map(item => (
          <li key={item.id}>
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}


疑難排解

我已發送一個 action,但日誌記錄顯示的是舊的狀態值

呼叫 dispatch 函式**並不會改變正在執行的程式碼中的狀態**

function handleClick() {
console.log(state.age); // 42

dispatch({ type: 'incremented_age' }); // Request a re-render with 43
console.log(state.age); // Still 42!

setTimeout(() => {
console.log(state.age); // Also 42!
}, 5000);
}

這是因為狀態的行為就像快照一樣。 更新狀態會請求使用新的狀態值進行另一次渲染,但不會影響已在執行的事件處理常式中的 state JavaScript 變數。

如果您需要猜測下一個狀態值,可以透過自行呼叫 reducer 來手動計算它

const action = { type: 'incremented_age' };
dispatch(action);

const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }

我已發送一個 action,但畫面沒有更新

如果下一個狀態等於前一個狀態(透過 Object.is 比較確定),React 將**忽略您的更新**。這通常發生在您直接更改狀態中的物件或陣列時

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Wrong: mutating existing object
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Wrong: mutating existing object
state.name = action.nextName;
return state;
}
// ...
}
}

您變更了現有的 state 物件並將其返回,因此 React 忽略了更新。要解決此問題,您需要確保始終更新狀態中的物件更新狀態中的陣列,而不是變更它們

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Correct: creating a new object
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ Correct: creating a new object
return {
...state,
name: action.nextName
};
}
// ...
}
}

發送 action 後,我的 reducer 狀態的一部分變成了 undefined

確保每個 case 分支在返回新狀態時**複製所有現有欄位**

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Don't forget this!
age: state.age + 1
};
}
// ...

如果上面沒有 ...state,則返回的下一個狀態將僅包含 age 欄位,而沒有其他欄位。


發送 action 後,我的整個 reducer 狀態變成了 undefined

如果您的狀態意外變成 undefined,您可能忘記在其中一個 case 中`return` 狀態,或者您的 action 類型與任何 case 陳述式都不匹配。要找出原因,請在 switch 外面拋出一個錯誤

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}

您也可以使用 TypeScript 之類的靜態類型檢查器來捕獲此類錯誤。


我收到一個錯誤:「太多次重新渲染」

你可能會遇到一個錯誤訊息:渲染次數過多。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 堆疊追蹤,找到導致錯誤的特定 dispatch 函式呼叫。


我的 Reducer 或初始化函式執行兩次

嚴格模式 中,React 會呼叫你的 Reducer 和初始化函式兩次。這不應該會破壞你的程式碼。

這種**僅限開發環境**的行為可以幫助你保持組件的純粹性。React 會使用其中一次呼叫的結果,並忽略另一次呼叫的結果。只要你的組件、初始化函式和 Reducer 函式都是純粹的,這就不會影響你的邏輯。但是,如果它們意外地不純粹,這可以幫助你注意到錯誤。

例如,這個不純粹的 Reducer 函式會改變狀態中的一個陣列。

function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Mistake: mutating state
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}

因為 React 會呼叫你的 Reducer 函式兩次,你會看到待辦事項被添加了兩次,這樣你就會知道有一個錯誤。在這個例子中,你可以透過替換陣列而不是改變它來修正錯誤。

function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ Correct: replacing with new state
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}

現在這個 Reducer 函式是純粹的,多次呼叫它不會改變行為。這就是為什麼 React 多次呼叫它可以幫助你找到錯誤的原因。**只有組件、初始化函式和 Reducer 函式需要是純粹的。** 事件處理程序不需要是純粹的,所以 React 不會呼叫你的事件處理程序兩次。

閱讀保持組件的純粹性以了解更多資訊。

... (保留前後頁連結,翻譯內容與英文版相同,因字數限制省略) ...