JavaScript
TypeScript
React
redux
AtomicDesign

俺の考えたRedux + Atomic DesignでTypeSafeにコンポーネントを扱う方法

自己紹介

じゅんじゅんと言うニックネームで、関西を拠点に活動しているフロントエンドエンジニアです。

HAL大阪の4年生です。(2018/10/18現在)

イベントや、勉強会に参加してるので是非お会いした際はお声掛けください!

俺の考えたRedux + Atomic DesignでTypeSafeにコンポーネントを扱う方法とは

まず最初に、ここでは僕がいま書いている業務のコードの設計を少し公開したいと思います。

ポエムです。閲覧注意しましょう。

そもそも Atomic Design を理解していないと思われるかもしれないので最初に謝っておきます。
ただ共通の認識としていてドキュメントにも記載しているのでこの方法で運用をしていこうと思っています。

ディレクトリ構成

プロダクトのディレクトリ構成はざっくり以下のようにつくりました。

- src/
    - actions/
        - hoge.ts
    - api/
        - client.ts
    - components/
    - model/
        - type.ts
    - store/
        - index.ts
        - reducer-type.ts
        - sagas/
        - reducers/

actions の中には ActionCreator を記述しています。API周り(axiosを使った通信)は client.ts にまとめています。
components の配下は Atomic Designを模したものになっています。
model/type.ts には Payload Response Domain というnamespaceで区切って型を管理しています。例えば、ログインをメアドとパスワードでするAPIを使う場合 Payload.Signin

interface Signin {
  email: string;
  password: string;
}

みたいになっています。これは後ほど Action のPayloadとして使う型です。
そのAPIのレスポンスを Response.Signin にまとめています。例えば

interface Signin {
  name: string;
  icon: string;
  token: string;
}

のような形です(これはサービスによって設計が違うと思うので言及しません、上記の型は実際僕が業務で書いているコードでもありません)

store 以下には combineReducer などを行なっている index.ts の他にReducerの定義をしている reducers やsagaを書いている sagas などがあります。

僕がこのプロジェクトで作った俺風な形を助けるのは reducer-type.ts です。
ファイル名は特に考えずに書いてしまったので reducer-type ですがちょっと適当すぎました。

reducer-type.tsの中身はなんなのか

reducer-type.ts には以下のような内容がかいてあります。

export interface ReduxAPIError {
  statusCode: string;
  message?: string;
}

interface ReduxAPIStruct<T> {
  isLoading: boolean;
  status: "success" | "failure";
  data: T;
  error: ReduxAPIError;
}

export const defaultSet = <T>(defaultValue?): ReduxAPIStruct<T> => ({
  isLoading: false,
  status: null,
  data: defaultValue || null,
  error: errorDefault()
});

export const errorDefault = (): ReduxAPIError => ({
  statusCode: null,
  message: ""
});

export default ReduxAPIStruct;

アプリケーション内ではこの便利ツールのことを ReduxAPIStruct と読んでいます。

重要なのは

interface ReduxAPIStruct<T> {
  isLoading: boolean;
  status: "success" | "failure";
  data: T;
  error: ReduxAPIError;
}

このinterfaceになります。

それぞれ、アプリケーションで通信を行なった場合の動きを記述しています。

isLoading はbooleanで管理していて、ロード中なのか否かを扱います。
status はのちにエラーコンポーネントへフォールバックしやすいようにつくってあります。
data は実際APIから取得できた時のデータ本体です。ここを T としてジェネリクスで定義してあります。この型はのちに使う時先ほどの model/type.tsResponse の型をそのまま渡します。
error には ReduxAPIError と言う型をいれています。

export interface ReduxAPIError {
  statusCode: string;
  message?: string;
}

ReduxAPIErrorstatusCodemessage を持ちます。
statusCode はそのAPIにアクセスした時のレスポンスのstatusCodeをいれます。messageはサーバーからのresponse bodyや後ほどsagaで入れ込むために型をもっておきます。

ReduxAPIStructがあるReactの世界

このReduxAPIStructがあるとどのようにコンポーネントを構築できるのか。

まずAtomic Designを模したものの重要な点として僕たちは pagestemplate の明確な責務の分離を行いました。
例えば、reduxのconnectをするのは paegs 級のコンポーネントだけにするなどです。

僕たちは pagesデータを持ちURLに紐づく単一のページ全体として定義しています。
一方 templateデータを前提につくられたUIとしてつくりました。

この明確な責務の分離により pages コンポーネントはこのようになります。

例えば、以下のようなデータを受け取る記事一覧ページをつくりたいと思います。

データは

{
  "items": [
    {
      "title": "タイトル1",
      "body"; "本文1"
    },
    {
      "title": "タイトル2",
      "body"; "本文2"
    },
    {
      "title": "タイトル3",
      "body"; "本文3"
    }
  ]
}

このAPIのエンドポイントを [GET] /items だとしてaxiosのAPI clientは以下のような実装だとします。

import axios from "axios";

class Client {
  public getItems() {
    return axios.get("/items");
  }
}

clientはPromiseを返すように実装します(その方が使う時に楽)

sagaのコードは端折りますが、結果を article reduceritems と言うキーにいれるとします。

import * as React from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router";
import { requestGetItems } from "~/actions/article";
import ErrorHandler from "~/ErrorHandler";

// templates
import ItemsPageTemplate from "~/components/templates/ItemsPage";
import LoadingTemplate from "~/components/templates/Loading";

// type
import { Response } from "~/model/type";
import ReduxAPIStruct from "~/store/reducer-type";

interface Props {
  items: ReduxAPIStruct<Response.Article.Items>;
}

class ItemPage extends React.Component {
  public render() {
    const { items } = this.props;
    if(items.status === "failure") {
      return <ErrorHandler errorStatus={items.error.statusCode}/>;
    }

    return items.isLoading ? <LoadingTemplate /> : <ItemsPageTemplate items={items.data} />;
  }
}

export default wituRouter(connect(
  state => ({ items: state.articleReducer.items })
)(ItemPage) as any);

このこれでデータが取得中であれば LoadingTemplate がでてデータのfetchに失敗した場合 ErrorHandler に流れて成功すれば ItemsPage がマウントされます。

この3種類の状態をどのページをかくときも何も考えないでも管理することができます。

ReduxAPIStructのおかげでそのデータをfetch中なのかそうでないのかを簡単に理解できて、fetchに失敗しているのかも status をみればすぐわかることができます。

この状態管理を別の状態へ切り出すことでsagaをかくときもそれに乗っ取りかけばコンポーネントのことを考えずに状態の管理を行うことができます。

あとがき

ReduxAPIStructとオレオレAtomic Designでコードを書くときに考えないといけないことは分散できましたが、ボイラーテンプレートが多くなったりするのがネックです。
例えば、Loadingなんていうのはどの画面でも同じようなmodulesを使うのであればもっとアプリケーションレベルで管理したほうが楽なのではないか、でもrouterの記述もあるしな。みたいなことを日々考えているので、もし俺の最強の大規模開発に向いている書き方!などがあれば教えてください(懇願)

上記のコードは業務で書いているコードから記事用に少し端折って書いているので、雑いところがありますが大目に見てもらえると助かります。

Twitter: @konojunya

コメントでもDMでも構わないので感想などまたもらえるとすごく嬉しいです。よろしくお願いします。