useReducer
useReducer
は、リデューサ をコンポーネントに追加するための React フックです。
const [state, dispatch] = useReducer(reducer, initialArg, init?)
リファレンス
useReducer(reducer, initialArg, init?)
リデューサ で状態を管理するために、コンポーネントのトップレベルで useReducer
を呼び出します。
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
引数
reducer
: 状態をどのように更新するかを指定するリデューサ関数です。純粋でなければならず、引数として state と action を取り、次の state を返します。state と action はどのような型でも大丈夫です。initialArg
: 初期状態が計算される元になる値です。任意の型の値を指定できます。どのように初期状態を計算するかは、次のinit
引数に依存します。- オプション
init
: 初期状態を返す初期化関数です。指定されていない場合、初期状態はinitialArg
に設定されます。そうでない場合、初期状態はinit(initialArg)
の結果が設定されます。
Returns
useReducer
は、2 つの値を持つ配列を返します:
- 現在の状態。最初のレンダリング中に、
init(initialArg)
またはinitialArg
(init
がない場合)が設定されます。 - 状態を別の値に更新し、再レンダーをトリガするための
dispatch
関数。
注意点
useReducer
はフックなので、コンポーネントのトップレベルまたは独自のカスタムフック内でのみ呼び出すことができます。ループや条件の中で呼び出すことはできません。必要な場合は、新しいコンポーネントとして抜き出し、その中に状態を移動させてください。- Strict Mode では、React は偶発的な不純物を見つけるのを助けるために、リデューサと初期化関数を 2 回呼び出します。これは開発時の動作であり、本番には影響しません。リデューサと初期化関数が純粋であれば(そうあるべきです)、これはロジックに影響しません。片方の呼び出しの結果は無視されます。
dispatch
関数
useReducer
によって返される dispatch
関数は、状態を別の値に更新し、再レンダーをトリガすることができます。dispatch
関数には、action を唯一の引数として渡す必要があります。
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...
React は、現在の state
と dispatch
に渡されたアクションを使用して、次の状態を reducer
関数を呼び出した結果に設定します。
引数
action
:ユーザによって実行されたアクションです。任意の型の値を指定できます。慣例として、アクションは通常オブジェクトで、type
プロパティで識別され、オプションで追加情報を他のプロパティで持ちます。
返り値
dispatch
関数には返り値はありません。
注意点
-
dispatch
関数は、次のレンダリングのための状態変数のみを更新します。dispatch
関数を呼び出した後に状態変数を読み取ると、呼び出し前の古い値が返されます。(古い状態の値が表示される) -
与えられた新しい値が、
Object.is
の比較により、現在のstate
と同じと判断された場合、React はコンポーネントとその子要素の再レンダーをスキップします。これは最適化です。React は結果を無視する前にコンポーネントを呼び出す必要があるかもしれませんが、コードには影響しないはずです。 -
React は 状態の更新をバッチ処理 します。これにより、すべてのイベントハンドラが実行され、それらの
set
関数が呼び出された後に画面が更新されます。これにより、1 つのイベント中に複数回の再レンダーが発生するのを防ぐことができます。レアケースとして、DOM にアクセスするために画面を早期に更新する必要があるなど、React に画面の更新を強制する必要がある場合は、flushSync
を使用できます。
使用法
コンポーネントにリデューサを追加する
コンポーネントのトップレベルで useReducer
を呼び出して、リデューサを使って状態を管理します。
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
useReducer
は、2 つの値を持つ配列を返します:
- この state 変数の現在の状態には、与えられた初期状態が初期値として設定されます。
- インタラクションに応じて状態を変更するための
dispatch
関数です。
画面上の内容を更新するには、アクションと呼ばれるユーザが行ったことを表すオブジェクトを引数としてdispatch
を呼び出します。
function handleClick() {
dispatch({ type: 'incremented_age' });
}
React は現在の状態とアクションをリデューサ関数に渡します。リデューサは次の状態を計算して返します。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' }) }}> 年齢を増やす </button> <p>こんにちは!あなたは{state.age}歳です。</p> </> ); }
useReducer
は useState
と非常に似ていますが、イベントハンドラから状態更新ロジックをコンポーネントの外の単一の関数に移動することができます。詳しくは、useState
と useReducer
の選び方を参照ください。
リデューサ関数の書き方
リデューサ関数は次のように宣言されます:
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
プロパティを持つオブジェクトを渡すことが一般的です。アクションにはリデューサが次の状態を計算するために必要な最小限の情報を含めるべきです。
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
});
}
// ...
アクションのタイプ名はコンポーネントの中で固有です。各アクションは、データの複数の変更につながる場合でも、単一のインタラクションを表します。状態の形は任意ですが、通常はオブジェクトまたは配列になります。
詳しくは、リデューサへの状態ロジックの抽出を参照ください。
例 1/3: フォーム(オブジェクト)
この例では、リデューサが 2 つのフィールド(name
と age
)を持つ状態オブジェクトを管理しています。
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
の 3 番目の引数として初期化関数を渡すことができます:
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...
createInitialState
を渡していることに注意してください。これは関数そのものであり、createInitialState()
ではありません。これにより、初期状態は初期化後に再作成されません。
上記の例では、createInitialState
は username
引数を受け取ります。初期化関数が初期状態を計算するために情報を必要としない場合は、useReducer
に対して 2 番目の引数として null
を渡すことができます。
例 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> </> ); }
トラブルシューティング
アクションをディスパッチしたが、ログには古い状態の値が表示されます
dispatch
関数を呼び出しても、実行中のコード内の状態は変更されません:
function handleClick() {
console.log(state.age); // 42
dispatch({ type: 'incremented_age' }); // 43 で再レンダーを要求
console.log(state.age); // まだ 42!
setTimeout(() => {
console.log(state.age); // またもや 42!
}, 5000);
}
これは 状態がスナップショットのように振る舞う ためです。状態を更新すると、新しい状態値で再レンダーが要求されますが、既に実行中のイベントハンドラ内の state
JavaScript 変数には影響を与えません。
次の状態値を推測する必要がある場合は、リデューサを自分で呼び出して手動で計算することができます:
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }
アクションをディスパッチしたが、画面が更新されません
React は、次の状態が前の状態と等しい場合、つまり Object.is
の比較によって判断される場合、更新を無視します。これは、状態内のオブジェクトや配列を直接変更した場合に通常発生します:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 間違い: 既存のオブジェクトを変更している
state.age++;
return state;
}
case 'changed_name': {
// 🚩 間違い: 既存のオブジェクトを変更している
state.name = action.nextName;
return state;
}
// ...
}
}
既存の state
オブジェクトを変更して返しているため、React は更新を無視します。これを修正するには、常に 状態内のオブジェクトを更新する ことと、状態内の配列を更新する ことを確認する必要があります:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ 正しい: 新しいオブジェクトを作成している
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ 正しい: 新しいオブジェクトを作成している
return {
...state,
name: action.nextName
};
}
// ...
}
}
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
};
}
// ...
}
}
ディスパッチ後に私のリデューサステート全体が未定義になります
ステートが予期せず undefined
になる場合、おそらくいずれかの case
ステートメントでステートを return
し忘れているか、アクションのタイプがいずれの case
ステートメントとも一致していない可能性があります。原因を見つけるために、switch
の外でエラーをスローしてください。
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}
また、このようなミスをキャッチするために、TypeScript のような静的型チェッカーを使用することもできます。
エラーが発生しています:“Too many re-renders”
Too many re-renders. React limits the number of renders to prevent an infinite loop.
というエラーが表示されることがあります。通常、これはレンダリング中にアクションを無条件でディスパッチしているため、コンポーネントがループに入っていることを意味します:レンダリング、ディスパッチ(これにより再度レンダリングが発生)、レンダリング、ディスパッチ(これにより再度レンダリングが発生)、などの繰り返しです。非常にしばしば、これはイベントハンドラの指定におけるミスによって引き起こされます。
// 🚩 間違い: レンダリング中にハンドラを呼び出しています
return <button onClick={handleClick()}>Click me</button>
// ✅ 正しい: イベントハンドラを渡します
return <button onClick={handleClick}>Click me</button>
// ✅ 正しい: インライン関数を渡します
return <button onClick={(e) => handleClick(e)}>Click me</button>
このエラーの原因がわからない場合は、コンソールのエラーの横にある矢印をクリックし、JavaScript スタックを調べてエラーの原因となる具体的な dispatch
関数呼び出しを見つけてください。
リデューサまたはイニシャライザ関数が 2 回実行されます
Strict Mode では、React はリデューサとイニシャライザ関数を 2 回呼び出します。これはコードを壊すことはありません。
この開発専用の振る舞いは、コンポーネントを純粋に保つための役割を果たします。React は 2 回の呼び出しのうちの 1 つの結果を使用し、もう 1 つの結果は無視します。コンポーネント、イニシャライザ、およびリデューサ関数が純粋である限り、これはロジックに影響を与えません。ただし、これらの関数が誤って不純である場合、これによりミスに気付くことができます。
例えば、次の不純なリデューサ関数はステート内の配列を変更しています:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 ミス: ステートを変更しています
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}
React がリデューサ関数を 2 回呼び出すため、todo が 2 回追加されたことがわかり、ミスがあることがわかります。この例では、配列を変更する代わりに配列を置き換えることでミスを修正できます:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ 正しい: 新しいステートで置き換える
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}
このリデューサ関数が純粋であるため、1 回余分に呼び出されても動作に影響はありません。これが React が 2 回呼び出す理由です。**コンポーネント、イニシャライザ、およびリデューサ関数のみが純粋である必要があります。**イベントハンドラは純粋である必要はないため、React はイベントハンドラを 2 回呼び出すことはありません。
詳細は、コンポーネントを純粋に保つを参照してください。