Interactive

Reducer Pattern

プロンプトに「Reducer」を含めることで、ClaudeCodeはコンポーネントと状態管理を疎結合にする実装を生成できます。弊社では、深いコンポーネント階層でのProps Drilling(バケツリレー)を避けたい場合や、状態更新ロジックをコンポーネントから分離したい場合にReducerパターンを使用します。

弊社の推奨ルール

  1. コンポーネントとロジックを分離 - 状態管理をビューから独立
  2. Props Drillingを回避 - 深い階層でも状態を直接共有
  3. 純粋関数にする - 副作用のない状態変更のみ
  4. イミュータブルな更新 - 新しい状態オブジェクトを返す
  5. アクション型を明確にする - 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または空配列を使用します。