はじめに
useReducer
を使って同様のステート管理を行うと、より複雑なステートの更新ロジックを扱う場合に、コードが整理されやすくなります。特に、複数の関連するステート変更が絡み合う場合に有効です。
例えば、ゲームに例えるとHPが一定以下になった時に能力が発動するような機能 が必要な時に重宝します。
useReducer
を使ってカウンターの状態管理を行う例を紹介します。ここでは、カスタムフックとコンテキストを組み合わせて、複数のコンポーネントで同じステートを共有するパターンで useReducer
を利用します。
サンプル
import React, { createContext, useContext, useReducer, useCallback } from 'react';
// アクションの型を定義
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
// reducer関数
const counterReducer = (state, action) => {
switch (action.type) {
case INCREMENT:
return { count: state.count + 1 };
case DECREMENT:
return { count: state.count - 1 };
default:
return state;
}
};
// カウンターのコンテキストを作成
const CounterContext = createContext(null);
// カウンターのステートとディスパッチ関数を提供するプロバイダー
const CounterProvider = ({ children }) => {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
const increment = useCallback(() => {
dispatch({ type: INCREMENT });
}, [dispatch]);
const decrement = useCallback(() => {
dispatch({ type: DECREMENT });
}, [dispatch]);
return (
<CounterContext.Provider value={{ count: state.count, increment, decrement, dispatch }}>
{children}
</CounterContext.Provider>
);
};
// カウンターのステートと関数を利用するためのカスタムフック
const useCounterContext = () => {
const context = useContext(CounterContext);
if (!context) {
throw new Error("useCounterContext must be used within a CounterProvider");
}
return context;
};
// カウンターを表示する子コンポーネント
const CounterDisplay = () => {
const { count } = useCounterContext();
return <div>現在のカウント: {count}</div>;
};
// カウントを増やすボタンを持つ子コンポーネント
const IncrementButton = () => {
const { increment } = useCounterContext();
return <button onClick={increment}>増やす</button>;
};
// カウントを減らすボタンを持つ子コンポーネント
const DecrementButton = () => {
const { decrement } = useCounterContext();
return <button onClick={decrement}>減らす</button>;
};
// 親コンポーネント
const ParentComponent = () => {
return (
<CounterProvider>
<CounterDisplay />
<IncrementButton />
<DecrementButton />
</CounterProvider>
);
};
export default ParentComponent;
変更点:
-
counterReducer
関数:- 現在のステートとアクションを受け取り、新しいステートを返します。
- アクションの
type
に基づいて、どのようにステートを更新するかを定義します。 -
INCREMENT
とDECREMENT
のアクションを処理します。
-
CounterProvider
コンポーネント:-
useReducer
フックを使って、state
とdispatch
関数を取得します。初期ステートは{ count: 0 }
です。 -
increment
関数とdecrement
関数は、それぞれ対応するタイプ (INCREMENT
,DECREMENT
) のアクションをdispatch
関数に渡します。useCallback
を使用して、依存配列にdispatch
を含めることで、dispatch
関数が変更された場合にのみ再生成されるように最適化しています。 - コンテキストプロバイダーの値として、
state.count
と、アクションをディスパッチするための関数 (increment
,decrement
,dispatch
) を提供します。
-
-
useCounterContext
カスタムフック:- コンテキストの値を取得し、それを返します。
-
子コンポーネント (
CounterDisplay
,IncrementButton
,DecrementButton
):-
useCounterContext
フックを通じて、共有されたcount
とincrement
,decrement
関数を利用します。
-
useReducer
を使うメリット:
-
複雑なステート管理: ステートの更新ロジックが複雑になるほど、
reducer
関数内でロジックを集中管理できるため、コードの見通しが良くなります。 - 予測可能なステート遷移: アクションを通じてのみステートが変更されるため、いつ、なぜステートが変更されたのかを追跡しやすくなります。
-
テストの容易性:
reducer
関数は純粋な関数であるため、入力(現在のステートとアクション)に対して常に同じ出力(新しいステート)が得られることを保証しやすく、単体テストが容易になります。
useReducer
を使うデメリット:
-
ボイラープレートコードが増える: シンプルなステート管理の場合でも、アクションの型定義や
reducer
関数の記述が必要になるため、コード量が増えることがあります。
どのような場合に useReducer
を使うべきか:
- ステートの更新ロジックが複数あり、相互に依存している場合。
- ステートの変更履歴を追跡したい場合(デバッグやUndo/Redo機能の実装など)。
- より予測可能でテストしやすいステート管理を目指したい場合。
今回のカウンターの例のように、単純なステートの増減だけであれば、useState
でも十分にシンプルに実装できます。しかし、より複雑なステート管理が必要になった場合には、useReducer
を検討することで、より堅牢で保守性の高いコードを書くことができるでしょう。