Reducer Pattern
プロンプトに「Reducer」を含めることで、ClaudeCodeはコンポーネントと状態管理を疎結合にする実装を生成できます。弊社では、深いコンポーネント階層でのProps Drilling(バケツリレー)を避けたい場合や、状態更新ロジックをコンポーネントから分離したい場合にReducerパターンを使用します。
弊社の推奨ルール
- コンポーネントとロジックを分離 - 状態管理をビューから独立
- Props Drillingを回避 - 深い階層でも状態を直接共有
- 純粋関数にする - 副作用のない状態変更のみ
- イミュータブルな更新 - 新しい状態オブジェクトを返す
- アクション型を明確にする - Union Typeで操作を制限
ClaudeCodeでの利用
Reducerパターンへの変換
既存の状態管理を集約して保守性を向上させたい場合
「この状態管理をReducerパターンに変更して。アクションとステートの型定義も追加」
破壊的変更の修正
状態の直接変更をイミュータブルな操作に変更する場合
「直接的な状態変更をReducerに置き換えて」
ドメイン特化Reducer
特定の業務ロジックに対応するReducerを作成する場合
「ショッピングカート用のReducerを作成」
Props Drillingの解消
深い階層での状態共有をコンテキストで解決する場合
「ReducerとContextを組み合わせてバケツリレーを解消して」
Props DrillingではなくReducerを使う
深いコンポーネント階層での状態共有を効率化するため、Reducerパターンで疎結合を実現します。Props Drillingは中間コンポーネントに不要な依存を生み、保守性を低下させます。
// ❌ 推奨しない:Props Drilling(バケツリレー)
function Layout({ user, cart, onAddToCart }) {
// userとcartを使わないが、子に渡すためだけに受け取る
return (
<div>
<Header user={user} cart={cart} />
<ProductList onAddToCart={onAddToCart} />
</div>
);
}
中間コンポーネントが状態を使わなくても、子に渡すためだけにpropsを受け取る必要があります。階層が深くなるほど、不要な依存が増えて保守性が低下します。
Reducerパターンで状態を集約
状態とアクションの型を定義し、状態更新ロジックを一箇所に集約します。
// 状態とアクションの型定義
type AppState = {
user: User | null;
cart: { items: Item[]; total: number };
};
type AppAction =
| { type: 'SET_USER'; user: User }
| { type: 'ADD_TO_CART'; item: Item }
| { type: 'REMOVE_FROM_CART'; id: string };
Reducerは純粋関数として状態変更ロジックを管理します。if文で各アクションを処理し、新しい状態オブジェクトを返します。
function appReducer(state: AppState, action: AppAction): AppState {
if (action.type === 'SET_USER') {
return { ...state, user: action.user };
}
if (action.type === 'ADD_TO_CART') {
return {
...state,
cart: {
items: [...state.cart.items, action.item],
total: state.cart.total + action.item.price
}
};
}
return state;
}
ContextとReducerの組み合わせ
ReducerとContextを組み合わせることで、どのコンポーネントからも状態にアクセスできるようになります。
// Contextの作成とProvider設定
const AppContext = createContext<{
state: AppState;
dispatch: (action: AppAction) => void;
}>({ state: initialState, dispatch: () => {} });
function App() {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
<Layout />
</AppContext.Provider>
);
}
中間コンポーネントはpropsを受け取る必要がなくなり、必要な場所で直接Contextから状態を取得できます。
function Layout() {
// propsを受け取る必要なし
return (
<div>
<Header />
<ProductList />
</div>
);
}
function ProductList() {
const { dispatch } = useContext(AppContext);
const handleAddToCart = (item: Item) => {
dispatch({ type: 'ADD_TO_CART', item });
};
return <button onClick={() => handleAddToCart(item)}>カートに追加</button>;
}
非同期処理を含むReducer
API呼び出しなどの非同期処理では、loading状態やerror状態も管理する必要があります。
// 非同期状態の型定義
type AsyncState<T> = {
data: T | null;
loading: boolean;
error: string | null;
};
type UserAction =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; users: User[] }
| { type: 'FETCH_ERROR'; error: string };
各アクションで適切に状態を更新し、ローディング中やエラー時の状態を管理します。
function userReducer(state: AsyncState<User[]>, action: UserAction) {
if (action.type === 'FETCH_START') {
return { ...state, loading: true, error: null };
}
if (action.type === 'FETCH_SUCCESS') {
return { data: action.users, loading: false, error: null };
}
if (action.type === 'FETCH_ERROR') {
return { ...state, loading: false, error: action.error };
}
return state;
}
非同期処理はReducerの外部で行い、結果をアクションでdispatchします。これによりReducerの純粋性を保ちます。
// 非同期処理とdispatch
async function fetchUsers(dispatch: (action: UserAction) => void) {
dispatch({ type: 'FETCH_START' });
try {
const users = await api.getUsers();
dispatch({ type: 'FETCH_SUCCESS', users });
} catch (error) {
dispatch({ type: 'FETCH_ERROR', error: error.message });
}
}
よくある問題
状態を直接変更してしまう
Reducerで状態を直接変更すると、Reactの再レンダリングが正しく動作しません。
// ❌ エラー:状態を直接変更
function reducer(state: State, action: Action) {
if (action.type === 'ADD_ITEM') {
state.items.push(action.item); // 破壊的変更
return state;
}
}
必ず新しいオブジェクトを返すことで、Reactが変更を検知できるようになります。
// ✅ 解決:新しいオブジェクトを返す
function reducer(state: State, action: Action): State {
if (action.type === 'ADD_ITEM') {
return {
...state,
items: [...state.items, action.item] // 新配列を作成
};
}
return state;
}
アクション型が型安全でない
anyやstringを使うと型安全性が失われます。Union Typeで厳密に定義します。
// ✅ Union Typeで型安全に
type Action =
| { type: 'ADD_ITEM'; item: Item }
| { type: 'REMOVE_ITEM'; id: string }
| { type: 'CLEAR_ALL' };
Redux等のライブラリは必要か
弊社では小~中規模なら素のReducerで十分としています。複雑になったらRedux Toolkitを検討します。
非同期処理の扱い方
Reducerは純粋関数にします。非同期処理は別関数で行い、結果をアクションでdispatchします。
初期状態の設計
各フィールドにデフォルト値を設定します。undefinedは避けてnullまたは空配列を使用します。