🐁

Reduxの推奨される記述の導入

2024/03/22に公開

はじめに

Redux Style GuideRedux Style Guide 日本語訳)によると
Reducerに可能な限りロジックを記述することが推奨されています。

推奨される記述の導入を促すのがこの記事の目的です。

Actionの命名について

まずは、Actionの命名規則について

【redux】reduxのベストプラクティス6選〜Action編〜

Action名は「システムが行うこと」ではなく「外の世界で実際に起こったこと」を書く。

Action名で「〇〇Init」「Unmount〇〇」「Set〇〇」などが使われることがあるかと思います。
これらは処理(中身)のことを指しているので推奨されていません。

USER_LOGOUTRESET_FORM などの状態変更を表す名前を使用しましょう。

Action名は、「実際に起こったこと」を記述することが推奨されます。
これは、開発者がコードを読む際に、そのActionが何を行うのか明確に理解できるようにするためです。

Actionの例

  • ユーザーがログインした
{  
    type: 'USER_LOGGED_IN',  
    payload: { username: 'example', password: 'password' }  
}
  • 商品をカートに追加した
{  
    type: 'ITEM_ADDED_TO_CART',  
    payload: { itemId: 123, itemName: 'Sample Item', quantity: 1 }  
}  
  • カートから商品を削除した
{  
    type: 'ITEM_REMOVED_FROM_CART',  
    payload: { itemId: 123 }  
} 

Action名は具体的な行動を表すことで、コードの可読性が向上します。

ActionCreatorとReducerの役割分担

ActionCreatorとReducer、それぞれの役割を理解しておきましょう。

  • ActionCreator
    • Actionの作成とDispatchを担当
    • 非同期処理やAPI呼び出しのような副作用のあるロジックを含む
  • Reducer
    • 状態の変更を担当
    • 純粋な関数であること

以上のことから、ActionCreatorでActionをDispatchする際はペイロードに余分な情報を持たせないようにしましょう。

Reducerについて

Reducerはアプリケーションの状態を管理するための重要な部分なので、複雑なロジックはReducer内で処理することが推奨されます。

Reducerの例

例えば、ユーザーがログインした際の状態を管理するReducerは以下のようになります。

const initialState = {  
    isAuthenticated: false,  
    user: {}  
};  
  
const authReducer = (state = initialState, action) => {  
    switch (action.type) {  
        case 'USER_LOGGED_IN': {
            return {  
                ...state,  
                isAuthenticated: true,  
                user: action.payload  
            };
        }
        case 'USER_LOGGED_OUT': {
            return {  
                ...state,  
                isAuthenticated: false,  
                user: {}  
            };
        }
        case 'USER_UPDATED_PROFILE': {
            return {  
                ...state,  
                user: {  
                    ...state.user,  
                    ...action.payload  
                }  
            };
        }
        default: {
            return state;
        }  
    }  
}  

ActionをDispatchする際にbooleanを渡すというActionCreatorにて真偽が決定される記述を見かけることがあるのですが、USER_LOGGED_INというAction名に基づけばログイン状態を表すisAuthenticatedはtrueにしかならないので、USER_LOGGEN_INのペイロードでbooleanを渡す必要はなくなり、Action名に関しても何を行うのか明確になります。

以下はもう少し複雑なロジックを含むひとつの例です。

interface CartItem {
    /**
     * 商品id
     */
    id: string;
    /**
     * 商品名
     */
    name: string;
    /**
     * 単価
     */
    price: number;
    /**
     * 数量
     */
    quantity: number;
}

interface State {
    cart: CartItem[];
}

const initialState: State = {
    cart: [],
};

const cartReducer = (state = initialState, action) => {
    const { cart } = state;

    switch (action.type) {
        case "ITEM_ADDED_TO_CART": {
            const { payload } = action;
            const { id, quantity } = payload;

            // 商品がすでにカートに存在するかを確認
            const existingItem = cart.some(item => item.id === id);

            // 商品がカートに存在する場合、数量を増やす
            if (existingItem) {
                const newCart = cart.map(item => {
                    const newValue =
                        item.id === id
                            ? // 同じidものだけ数量を増やす
                              { ...item, quantity: item.quantity + quantity }
                            : item;

                    return newValue;
                });

                return {
                    ...state,
                    cart: newCart,
                };
            }

            // 商品がカートに存在しない場合、新たに追加
            return {
                ...state,
                cart: [...cart, payload],
            };
        }
        case "ITEM_REMOVED_FROM_CART": {
            const {
                payload: { id },
            } = action;

            // 商品をカートから削除
            const newCart = cart.filter(item => item.id !== id);

            return {
                ...state,
                cart: newCart,
            };
        }
        case "CART_CLEARED": {
            // カートをクリア
            return {
                ...state,
                cart: [],
            };
        }
        case "QUANTITY_UPDATED": {
            const {
                payload: { id, quantity }
            } = action;

            // カート内の商品の数量を更新
            const newCart = cart.map(item => {
                const newItem =
                    item.id === id
                        ? // 同じidのものだけ数量を更新する
                          { ...item, quantity: quantity }
                        : item;

                return newItem;
            });

            return {
                ...state,
                cart: newCart,
            };
        }
        default: {
            return state;
        }
    }
};
  • ITEM_REMOVED_FROM_CART
    • ペイロードで指定されたIDの商品をカートから削除
    • IDが一致しない商品だけを新しいカートとする
  • CART_CLEARED
    • カートをクリア
    • カートの状態を空の配列でリセット
  • QUANTITY_UPDATED
    • カート内の特定の商品の数量を更新
    • ペイロードで指定されたIDの商品を見つけたらその数量を更新

Reducerにロジックを記述するメリット

  • 一貫性
    • アプリケーション全体で状態管理のロジックが一箇所に集約されるため、一貫性を保つことができる
    • 同じ操作が複数の場所で異なる方法で行われるという問題を避けることができる
  • 可読性と保守性
    • 状態変更のロジックが中心的な場所にまとめられているため、コードの可読性と保守性が向上
    • 新しい開発者がチームに参加した場合でも、アプリケーションの主要なビジネスロジックがどこにあるかをすぐに理解することができる
  • テスト容易性
    • Reducerは純粋な関数であるため、テストが容易
    • 与えられた入力に対して常に同じ出力を返すため、ユニットテストを書くのが比較的簡単
  • 分離と集中
    • アプリケーションの状態変更ロジックをReducerに集中させることで、その他の部分(ActionCreatorやコンポーネント)はUIロジックやイベントハンドリングに集中できる

まとめ

適切にActionCreatorとReducerを使うことで、可読性やデバッグの容易さが大幅に向上します。
それぞれの役割と責任を理解し、適切に使用することで、より堅牢で管理しやすいアプリケーション開発に繋がるかと思います。

参考

【redux】reduxのベストプラクティス6選〜Action編〜 #JavaScript - Qiita

Thinkingsテックブログ

Discussion