useReducer
は、コンポーネントに reducer を追加できるReactフックです。
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関数。ピュアでなければならず、状態とアクションを引数に取り、次の状態を返す必要があります。状態とアクションは任意の型にすることができます。initialArg
: 初期状態が計算される値。任意の型の値にすることができます。初期状態がどのように計算されるかは、次のinit
引数によって異なります。- オプション
init
: 初期状態を返す初期化関数。指定されていない場合、初期状態はinitialArg
に設定されます。そうでない場合、初期状態はinit(initialArg)
を呼び出した結果に設定されます。
戻り値
useReducer
は、正確に2つの値を持つ配列を返します。
- 現在の状態。初回レンダリング時には、
init(initialArg)
またはinitialArg
(init
がない場合)に設定されます。 dispatch
関数 を使用すると、状態を別の値に更新し、再レンダリングをトリガーできます。
注意点
useReducer
はフックなので、コンポーネントの最上位レベルまたは独自のフックでのみ呼び出すことができます。ループ内や条件内では呼び出すことはできません。必要であれば、新しいコンポーネントを作成し、状態をそこに移動してください。dispatch
関数は安定した同一性を持ちます。そのため、多くの場合、Effect の依存関係から省略されますが、含めても Effect が発火する原因にはなりません。リンターが依存関係をエラーなく省略することを許可する場合は、安全に省略できます。Effect の依存関係の削除について詳しく学ぶ。- 厳格モードでは、React はリデューサと初期化関数を2回呼び出します。これは偶発的な不純物を検出するのに役立ちます。これは開発時のみの動作であり、本番環境には影響しません。リデューサと初期化関数が純粋(であるべき)であれば、これはロジックに影響を与えません。呼び出しのいずれかからの結果は無視されます。
dispatch
関数
useReducer
によって返される dispatch
関数を使用すると、状態を別の値に更新し、再レンダリングをトリガーできます。dispatch
関数には、引数としてアクションを1つだけ渡す必要があります。
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...
React は、提供された reducer
関数を、現在の state
と dispatch
に渡されたアクションと共に呼び出した結果を、次の状態に設定します。
パラメータ
action
: ユーザーによって実行されたアクションです。任意の型の値にすることができます。慣例により、アクションは通常、それを識別するtype
プロパティと、オプションで追加情報を提供するその他のプロパティを持つオブジェクトです。
戻り値
dispatch
関数は戻り値を持ちません。
注意点
-
dispatch
関数は次のレンダリングでのみ状態変数を更新します。dispatch
関数を呼び出した後に状態変数を読み取ると、呼び出し前に画面に表示されていた古い値を取得します。 -
Object.is
比較によって決定されるように、提供する新しい値が現在のstate
と同一である場合、React はコンポーネントとその子の再レンダリングをスキップします。これは最適化です。React は結果を無視する前にコンポーネントを呼び出す必要がある場合がありますが、コードに影響を与えることはありません。 -
React は状態更新のバッチ処理を行います。すべてのイベントハンドラが実行され、その
set
関数を呼び出した後に画面を更新します。これにより、1つのイベント中に複数の再レンダリングが行われるのを防ぎます。まれに、DOMにアクセスするためにReactに早期に画面を更新させる必要がある場合は、flushSync
を使用できます。
使用方法
コンポーネントへのreducerの追加
コンポーネントの最上位レベルでuseReducer
を呼び出して、reducerを使用して状態を管理します。
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
useReducer
は、正確に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> </> ); }
useReducer
はuseState
と非常によく似ていますが、状態更新ロジックをイベントハンドラーからコンポーネント外の単一関数に移動できます。useState
とuseReducer
のどちらを選択するかについて詳しく読むことができます。
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に抽出するを参照してください。
例 1の 3: フォーム(オブジェクト)
この例では、reducerはname
とage
の2つのフィールドを持つ状態オブジェクトを管理します。
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
引数を取ります。初期化子に初期状態を計算するために情報が必要ない場合は、useReducer
の第二引数に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' }); // Request a re-render with 43
console.log(state.age); // Still 42!
setTimeout(() => {
console.log(state.age); // Also 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': {
// 🚩 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
};
}
// ...
}
}
ディスパッチ後にリデューサー状態の一部が未定義になります
case
ブランチのそれぞれで、新しい状態を返す際に既存のフィールドをすべてコピーしてください。
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Don't forget this!
age: state.age + 1
};
}
// ...
上記の...state
がないと、返される次の状態にはage
フィールドのみが含まれ、それ以外は何も含まれません。
ディスパッチ後にリデューサー状態全体が未定義になります
もし状態が予期せずundefined
になった場合、いずれかのケースで状態を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.
通常、これはレンダリング中に無条件にアクションをディスパッチしていることを意味し、コンポーネントがループに入ります。レンダリング、ディスパッチ(レンダリングが発生する)、レンダリング、ディスパッチ(レンダリングが発生する)、などです。多くの場合、これはイベントハンドラの指定ミスが原因です。
// 🚩 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
関数の呼び出しを特定してください。
リデューサまたは初期化関数が2回実行されます
StrictModeでは、Reactはリデューサと初期化関数を2回呼び出します。これはコードを壊すことはありません。
この開発時のみの動作は、コンポーネントをピュアに保つのに役立ちます。Reactは呼び出しの1つの結果を使用し、もう1つの結果を無視します。コンポーネント、初期化関数、およびリデューサ関数がピュアである限り、これはロジックに影響を与えません。ただし、誤ってインピュアになっている場合、この機能によってミスに気付くことができます。
たとえば、このインピュアなリデューサ関数は、状態内の配列を変更します。
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Mistake: mutating state
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}
Reactはリデューサ関数を2回呼び出すため、todoが2回追加されたことがわかります。つまり、間違いがあることがわかります。この例では、配列を変更する代わりに置き換えることで、間違いを修正できます。
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 }
]
};
}
// ...
}
}
これでこのリデューサ関数はピュアになったため、余分な回数を呼び出しても動作に違いはありません。これが、Reactが2回呼び出すことでミスを見つけるのに役立つ理由です。ピュアである必要があるのは、コンポーネント、初期化関数、およびリデューサ関数のみです。イベントハンドラはピュアである必要がないため、Reactはイベントハンドラを2回呼び出すことはありません。
詳細については、コンポーネントをピュアに保つを参照してください。