Reactで状態管理が複雑になってきたとき、useStateだけでは管理しきれないケースがあります。そんなときに登場するのがuseReducerです。この記事ではusereducerとは 使い方の観点から、基礎から活用パターンまでサンプルコード付きで丁寧に解説します。初心者にも実践者にも役立つ内容です。Reactフックをより自由自在に使いたい方は必読です。
目次
usereducerとは 使い方とは何か:基本の定義と目的
useReducerとはReactのフックの一種で、状態(state)とそれを更新するロジックを切り離して管理するための仕組みです。使い方としては、初期状態とアクションを受け取って新しい状態を返す純粋な関数(reducer)を定義し、それをuseReducerに渡すことでdispatch関数を取得します。状態の複雑な更新処理があるコンポーネントで、処理を明確かつ再利用可能にするために使われます。
使い方の目的は、state更新の条件が多岐にわたるとき、action typeによって処理を切り替えることで可読性と保守性を向上させることです。useStateでは複数のstateを分けたりオブジェクトにまとめたりしますが、更新ロジックが散らばると管理が難しくなります。useReducerではそのようなロジックを一箇所にまとめ、複数のアクションを扱う状態遷移を整理できます。
useReducerの基本構造
useReducerの構造は主に以下の要素から成ります。まずreducer関数は、現在のstateとactionを引数に取り、新しいstateを返す純粋関数であることが重要です。actionはtypeやpayloadなどのプロパティを持ち、処理内容を識別します。dispatch関数はactionをreducerに送る役割です。初期状態(initialState)も定義が必要です。
構文例としては「const [state, dispatch] = useReducer(reducer, initialState)」という形になります。stateが現在の状態、dispatchがアクションを送るための関数です。actionはオブジェクトでtypeや必要ならpayloadを含めることが一般的です。これにより状態遷移のルールが明確になります。
useReducerを使う意味とメリット
複数の状態変数を扱うコンポーネント、または状態がネストした構造を持つ場合、useReducerを用いることで更新ロジックが散らばらず一箇所にまとめられます。これにより可読性が高まり、コンポーネントの責務が整理されます。また、アクションタイプごとの処理が分かれているためバグが起きにくく、テストがしやすくなるメリットもあります。
さらにdispatch関数は安定した参照を保持するため、子コンポーネントに渡しても余計な再レンダリングが起きにくくなります。パフォーマンス観点でも、useStateで複数の状態を更新するケースではレンダリング回数が多くなることがありますが、useReducerでは一箇所でまとめて処理できるケースがあり効率が良くなることがあります。
useStateとの比較:いつ使い分けるか
使い分けの基準としては、状態の数・状態が相互に依存しているか・更新パターンが複雑かどうかが鍵です。単一の状態、例えばtrue/falseや単純な文字列・数値のようなものならuseStateの方が簡単で向いています。対して、複数のフィールドを持つフォーム、階層構造のデータ、もしくはアクションによって状態の各要素に対する処理が異なる場面ではuseReducerが適しています。
useStateは手軽ですが、更新関数が複数あるとコードが冗長になることがあります。useReducerはアクションをdispatchするだけでロジックが一元化され、switch文などで処理を分岐させることで状態変化が明確になります。これにより保守性・見通しが改善します。
使い方のステップバイステップ:基本から応用までのコード例
ここではuseReducerを実際にどう使うかを基本から応用までサンプルコード付きで説明します。最初は簡単なカウンターから始め、フォーム管理、非同期処理やコンテキストとの組み合わせまで幅広く扱います。最新情報を元に、React 18以降でも有効な使い方を紹介します。
基礎:カウンターの例
まずは基本的な例として、カウンターコンポーネントを考えます。stateにcountを持ち、増加・減少・リセットの3種類のアクションを定義します。
例:
コード例:
function Counter(){
const initialState = { count:0 };
function reducer(state, action){
switch(action.type){
case ‘increment’: return { count: state.count + 1 };
case ‘decrement’: return { count: state.count – 1 };
case ‘reset’: return { count: 0 };
default: return state;
}
}
const [state, dispatch] = useReducer(reducer, initialState);
return (
);
}
このように使うことで、状態と処理が明確に分離されます。count以外の状態が増えても、reducer内でswitchを拡張するだけで対応できます。
フォーム入力管理:複数フィールドの同期処理
次に複数フィールドを持つフォームで、入力値の更新・バリデーション・送信中状態などをまとめて管理する例を示します。フィールドが増えるほどuseStateで個別管理すると可読性が低下します。
例:
const initialFormState = { username:”, email:”, password:”, errors:{} , isSubmitting:false }
function formReducer(state, action){
switch(action.type){
case ‘CHANGE_FIELD’:
return { …state, [action.field]: action.value };
case ‘SET_ERRORS’:
return { …state, errors: action.errors };
case ‘SUBMIT_START’:
return { …state, isSubmitting:true };
case ‘SUBMIT_SUCCESS’:
return { …state, isSubmitting:false };
case ‘SUBMIT_FAILURE’:
return { …state, isSubmitting:false, errors:action.errors };
default:
return state;
}
}
このパターンでは、フォームの全ての状態を一つのstateオブジェクトで扱い、fed fieldsの変更・送信などに応じてdispatchを呼び出すことで管理します。codeの重複が減り、バリデーションロジックや非同期処理との組み合わせもやりやすくなります。
非同期処理との組み合わせ:fetchやAPI呼び出し時の状態管理
API呼び出しなど非同期処理が絡む場合、loading・success・errorといった状態を管理する必要があります。useReducerを使えばこれらを一元管理できます。
例:
const initialFetchState = { data:null, loading:false, error:null }
function fetchReducer(state, action){
switch(action.type){
case ‘FETCH_INIT’:
return { …state, loading:true, error:null };
case ‘FETCH_SUCCESS’:
return { …state, loading:false, data:action.payload };
case ‘FETCH_FAILURE’:
return { …state, loading:false, error:action.error };
default:
return state;
}
}
このような構造なら、dispatchを使って異なるフェーズの状態を明確に切り替えることができ、UIもloading時、成功時、失敗時で適切に分けて表示できます。テストやデバッグもしやすくなります。
使い方の注意点と応用パターン:パフォーマンス・可読性・拡張性
基本と応用を理解した上で、より実践的に使うために注意すべきポイントや応用パターンを紹介します。色々なケースで使われており、効率的な使い方が知られています。最新情報に基づいたベストプラクティスを交えて解説します。
パフォーマンス最適化:dispatchの安定性やレンダリングの制御
dispatch関数は再レンダリングしても同じ参照を保つ特性があります。これにより子コンポーネントに渡す際の余計なレンダリングを防ぎやすいです。他方、useStateで複数の状態を一度に更新するような複雑なロジックでは再レンダリングが多発しがちですが、useReducerでまとめて処理するとレンダリング回数を抑えやすくなります。
ただし、reducer関数が重たい処理を含むときは注意が必要です。例えば大きな配列操作や多重ネストのオブジェクト変更などの場合には、pureな処理とimmutabilityを保つことを意識し、必要ならmemoizationやuseCallback等の補助フックと組み合わせると良いでしょう。
コンテキストとの組み合わせ:グローバル状態管理の代替として
コンポーネントツリーの深い階層で同じ状態を使いたいとき、コンテキスト(Context)とuseReducerを組み合わせるパターンがよく使われます。グローバルストアのように振る舞わせることができ、Reduxなどのライブラリを使わずに済むことがあります。ただしProviderの値変更により配下の全コンポーネントが再レンダリングされる可能性があるため、必要なら状態を分割するかコンテキストを複数に分ける工夫が必要です。
また、カスタムフックとしてuseReducerを内包したコンテキストプロバイダーを作ると、ロジックを外部に隠しつつ複数のコンポーネントで状態を共有できてとても便利です。テスト・保守性・型安全性も向上します。
拡張パターン:type safetyや再利用可能なアクション定義
特にTypeScript使用時には、action typeをenumや定数で定義するパターンが多く見られます。payloadの型を明確にしておくことで間違いを防げます。アクションごとのインターフェースを定義するなどして、reducer内で扱うデータの型保証を確立させると良いでしょう。
また、アクションcreator関数を用意してdispatchを呼ぶ箇所を統一するパターンも有効です。これによりミスが減り、また将来的な拡張や状態変更時の影響範囲がわかりやすくなります。
他の状態管理手法との比較:useReducerとReduxなどの違い
ReactではuseStateやuseReducer以外にもReduxや外部の状態管理ライブラリが使われることがあります。それぞれの特徴を比較することで、どの場面にどれを採用するかの判断ができるようになります。最新のReactのバージョンにおける比較ポイントにも注目します。
useReducer vs Reduxの位置づけ
Reduxはグローバルな状態管理を目的とするライブラリで、アプリケーション全体に状態を共有し、副作用ミドルウェアやtime travel debuggingなど高度な機能が含まれることが多いです。一方、useReducerはローカルなコンポーネント状態やContextを使った限定的な共有状態に強く、機能はシンプルですが設定も軽く抑えられます。
中小規模アプリケーションではuseReducer + Contextの組み合わせで十分なことが多く、あえてReduxを導入する必要がないケースも多々あります。逆に、大規模アプリケーションで大量の状態更新・ミドルウェアサポート・デバッグ性などが求められる場合はReduxの方が向くことがあります。
useStateとの使い分けリスト
以下は使い分けのポイントを比較した表です。
| 状況 | useStateが適切 | useReducerが適切 |
|---|---|---|
| 状態の数が少ない/単純な状態 | 単一の boolean や数字、テキストなど | 複数フィールド・オブジェクト形式の状態 |
| 依存関係が少ない更新 | 他の状態に依存しない更新処理 | 以前の state を元にした処理が必要 |
| 可読性・簡潔さ重視 | 軽い機能・学習コストを抑えたいとき | ロジックをまとめて整理したいとき |
最新情報:Reactの内部実装やフックの変更
ReactではuseStateとuseReducerの実装が近年改善されており、内部的にはuseStateがシンプルなreducerラッパーであるという見方がされています。これにより、dispatchの参照が安定している点など、コンポーネント間での最適化しやすさが改めて注目されています。
また、初期状態(initialState)を関数で遅延初期化(lazy initialization)する機能や、state初期値を第三引数(init)で指定するパターンなども活用されており、大規模アプリや初期描画コストを抑えたい場面で効果を発揮しています。
まとめ
useReducerとは、Reactで状態と状態更新ロジックを一元管理し、複雑な状態変化を明確に扱うためのフックです。usereducerとは 使い方を理解することで、状態の可読性・保守性・パフォーマンスの向上が期待できます。単純な更新ならuseState、複数フィールドや非同期処理が絡むならuseReducerが適しています。
基本的なカウンターからフォーム管理、非同期処理、コンテキストとの組み合わせまで幅広く使えるパターンを紹介しました。最新情報に基づく内部実装の特徴やベストプラクティスにも触れましたので、実践にすぐ活かせる内容になっているはずです。
まずはご自身のプロジェクトで簡単な形で導入し、必要に応じてアクション定義やreducer設計を洗練させていくことをおすすめします。状態管理の曖昧さが消え、コードの見通しが格段に良くなるはずです。
コメント