「AWS無料相談会」をオンラインで開催中

ReduxをREST APIと使う時の命名のコツ

概要

今回は、REST APIに対してReduxを使用する場合の命名のコツを書いていきたいと思う。

リソースにあわせてconstantsをつくる

前準備として、リクエストの状態を表すconstantsをつくっておく。

基本的にREST APIを扱う時は、ロジック側はAPIリソースに合わせた命名にしておく。こうすることで、サーバーサイドの世界とコンテキストを切り替えることなく作業できて、混乱を避けることができる。例えば、ユーザー一覧を取得する/usersというエンドポイントがあれば、usersというオブジェクトで全体を設計してゆく。

Reduxで非同期な処理を扱う時は、リクエスト中 リクエスト成功 リクエスト失敗のような状態を持っておくと思うが、REST APIの場合、これにGET POST PUT DELETEなどがのってくるので、命名で混乱しないように、下記のようなヘルパーを作っておく。

const createAsyncConstants = (constant: string): TAsyncConstants => (
  {
    REQUEST: constant,
    SUCCESS: `${constant}_SUCCESS`,
    FAILURE: `${constant}_FAILURE`,
  }
);

export const createRESTConstants = (constant: string): TRESTConstants => (
  {
    GET: createAsyncConstants(`${constant}_GET`),
    POST: createAsyncConstants(`${constant}_POST`),
    PUT: createAsyncConstants(`${constant}_PUT`),
    DELETE: createAsyncConstants(`${constant}_DELETE`),
  }
);

export const USERS = createRESTConstants('USERS'); // 使用例

これがあると、export const USERS = createRESTConstants('USERS');のように書くだけで、USERS.GET.REQUEST USERS.GET.SUCCESS USERS.GET.FAILUREのようにそれぞれのメソッドに対応した非同期の状態を作ることができる。型がつくとサジェスチョンがでるので書き間違えることもない。

Reduxのstate設計をする

ユーザー一覧をつくるstateは以下のようになる。

const defaultUsers: TDefaultUsers = {
  items: [],
  query: {
    name: '',
    email: '',
    phoneNumber: '',
    type: '',
    emailVerifiedFlg: true,
    page: 1,
  },
  totalPage: 1,
  isLoading: false,
  errorMessage: '',
};

注意点としては、ここでのusersという命名は、エンドポイント(/users)にあわせたものではないということだ。ここはビューで扱いやすいようなモデルを設計するのが正解かと思う。

例えば、エンドポイント/usersPOSTメソッドを考えたときに、単一のユーザーを作成するフォーム画面があると想定する。

この場合、ビューには単一のユーザー情報のみが表されているので、投影するモデルは、usersではなく、userであり、フォームのデータはそれを構成するクエリになるのではないだろうか。stateは以下のようになりそうだ。

const defaultUser: TDefaultUser = {
  query: {
    name: '',
    email: '',
    phoneNumber: '',
    type: '',
    emailVerifiedFlg: true,
  },
  isLoading: false,
  errorMessage: '',
};

ビューに対してモデル設計をすると、同じようなビューが現れた時に使いまわしができる。例えば、この場合だと、ユーザーの編集画面(/users/:id PUT)が似たようなビューになるかもしれない。このように、ビューとAPIリソースが一致するとは限らないため、stateはビューに対して設計した方がいい場合が多い。

actionCreator, reducerをつくる

ducksパターンactionCreatorreducerを一気に作る(厳密にはもともとのducksパターンからは若干変えている部分がある)。immutableな更新については今回は省略している。

// ducksパターンをやりやすくするために`redux-actions`をいれている
import { createActions, handleActions } from 'redux-actions';
import {USERS} from './path/to/constants/';

export const usersActionCreators: TUsersActionCreators = createActions({
  [USERS.GET.REQUEST]: null,
  [USERS.GET.SUCCESS]: (items, totalPage) => {
    return {items, totalPage};
  },
  [USERS.GET.FAILURE]: (errorMessage) => {
    return {errorMessage};
  },
});

export const usersReducer = handleActions({
  [USERS.GET.REQUEST]: (state) => {
    return {
      ...state,
      isLoading: true,
      errorMessage: '',
    };
  },
  [USERS.GET.SUCCESS]: (state, {payload: {items, totalPage}}) => {
    // APIレスポンスから`items`, `totalPage`をもらう
    return {
      ...state,
      items,
      totalPage,
      isLoading: false,
      errorMessage: '',
    };
  },
  [USERS.GET.FAILURE]: (state, { payload: { errorMessage } }) => {
    return {
      ...state,
      isLoading: false,
      errorMessage,
    };
  },
}, defaultUsers);

上記のusersActionCreatorsは、redux-actionsにより、以下のようなメソッドを持つ。

export type TUsersActionCreators = {
  usersGet: (query: TUsersGetQuery) => void;
  usersGetSuccess: (items: Array<TUser>, totalPage: number) => void;
  usersGetFailure: (errorMessage: string) => void;
}

メソッドはこちらで作成したconstantscamelCaseになったものが作られるため、命名としてはAPIリソースにあわせたものになるのがわかる。ポイントとしては、リクエストの状態を表すものはAPIリソースに合わせた命名をして、Redux stateは描画したいDOMにあわせて命名、モデル設計をするということである。

APIリクエストのロジック

非同期な処理をどうするかというのはredux-sagaredux-thunkredux-observableなど、いくつかやり方があると思うが、個人的には責務が分離できてチームが使いやすければどれを使っても(使わなくても)いいと考える。ただし、命名は、サーバーサイドどダイレクトに通信する部分なので、APIリソースに合わせた命名をする。

実際にリクエストを行うサービスは、以下のようになる。

export const usersResource = {
  get: (query: TUsersGetQuery) => {
    // JS側では`camelCase`を使用しているので、`snake_case`に変換している
    const q = toSnakeCase(query);
    return axios.get(`${HOST}/users`, {params: q})
      .then((response) => {
        // 弊社では以下の指針でAPI設計しているためエラー時の処理を切り分ける
        // http://blog.mmmcorp.co.jp/blog/2015/07/01/web_api_guideline/
        if (response.data.status !== 200) {
          return {
            status: response.data.status,
            message: response.data.message,
          };
        }

        return {
          status: response.data.status,
          message: response.data.message,
          items: response.data.users.map((user) => {
            // APIレスポンスでは`snake_case`が返ってくるので`camelCase`に変換している
            return toCamelCase(user);
          }),
          totalPage: response.data.total_page,
        };
      })
      .catch(() => {
        return {
          status: 500,
          message: 'サーバーエラーが発生いたしました。しばらく時間が経ってから再度お試しください。',
        };
      });
  },
  // post: () => {}
  // delete: () => {}
  // put: () => {}
};

/usersリソースとのやりとりは、基本的にどのメソッドでもusersResourceを通して行うというイメージだ。これが必要な箇所でそれぞれ使われる。

これらの準備ができたらあとはDOMにつなぎこむだけである。

まとめ

今回の命名指針をまとめると、以下のようになる。

  • リソースにあわせて命名
    • constants
    • actionCreatorのメソッド
    • resourceサービス
  • DOMで使いやすいように命名
    • Redux state

実際は、Reduxが、サーバーサイド側のリソース設計とDOMへのモデル設計の橋渡し的な役割を持つ感じである。これを応用していくと多少複雑になっても命名に迷わなくてすむ。名前が決まるとモデル設計も見えてくるので、開発のスピードも上がるかもしれない。

以上、REST APIに対してReduxを使用する場合の命名のコツを紹介しました。