俺の考えたRedux + Atomic DesignでTypeSafeにコンポーネントを扱う方法
俺の考えたRedux + Atomic DesignでTypeSafeにコンポーネントを扱う方法:
じゅんじゅんと言うニックネームで、関西を拠点に活動しているフロントエンドエンジニアです。
HAL大阪の4年生です。(2018/10/18現在)
イベントや、勉強会に参加してるので是非お会いした際はお声掛けください!
まず最初に、ここでは僕がいま書いている業務のコードの設計を少し公開したいと思います。
ポエムです。閲覧注意しましょう。
そもそも Atomic Design を理解していないと思われるかもしれないので最初に謝っておきます。
ただ共通の認識としていてドキュメントにも記載しているのでこの方法で運用をしていこうと思っています。
プロダクトのディレクトリ構成はざっくり以下のようにつくりました。
みたいになっています。これは後ほど
そのAPIのレスポンスを
のような形です(これはサービスによって設計が違うと思うので言及しません、上記の型は実際僕が業務で書いているコードでもありません)
僕がこのプロジェクトで作った俺風な形を助けるのは
ファイル名は特に考えずに書いてしまったので
アプリケーション内ではこの便利ツールのことを
重要なのは
このinterfaceになります。
それぞれ、アプリケーションで通信を行なった場合の動きを記述しています。
このReduxAPIStructがあるとどのようにコンポーネントを構築できるのか。
まずAtomic Designを模したものの重要な点として僕たちは
例えば、reduxのconnectをするのは
僕たちは
一方
この明確な責務の分離により
例えば、以下のようなデータを受け取る記事一覧ページをつくりたいと思います。
データは
このAPIのエンドポイントを
clientは
sagaのコードは端折りますが、結果を
このこれでデータが取得中であれば
この3種類の状態をどのページをかくときも何も考えないでも管理することができます。
ReduxAPIStructのおかげでそのデータをfetch中なのかそうでないのかを簡単に理解できて、fetchに失敗しているのかも
この状態管理を別の状態へ切り出すことでsagaをかくときもそれに乗っ取りかけばコンポーネントのことを考えずに状態の管理を行うことができます。
ReduxAPIStructとオレオレAtomic Designでコードを書くときに考えないといけないことは分散できましたが、ボイラーテンプレートが多くなったりするのがネックです。
例えば、Loadingなんていうのはどの画面でも同じようなmodulesを使うのであればもっとアプリケーションレベルで管理したほうが楽なのではないか、でもrouterの記述もあるしな。みたいなことを日々考えているので、もし俺の最強の大規模開発に向いている書き方!などがあれば教えてください(懇願)
上記のコードは業務で書いているコードから記事用に少し端折って書いているので、雑いところがありますが大目に見てもらえると助かります。
Twitter: @konojunya
コメントでもDMでも構わないので感想などまたもらえるとすごく嬉しいです。よろしくお願いします。
自己紹介
じゅんじゅんと言うニックネームで、関西を拠点に活動しているフロントエンドエンジニアです。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; }
それぞれ、アプリケーションで通信を行なった場合の動きを記述しています。
isLoading
はbooleanで管理していて、ロード中なのか否かを扱います。status
はのちにエラーコンポーネントへフォールバックしやすいようにつくってあります。data
は実際APIから取得できた時のデータ本体です。ここを T
としてジェネリクスで定義してあります。この型はのちに使う時先ほどの model/type.ts
の Response
の型をそのまま渡します。error
には ReduxAPIError
と言う型をいれています。export interface ReduxAPIError { statusCode: string; message?: string; }
ReduxAPIError
は statusCode
と message
を持ちます。statusCode
はそのAPIにアクセスした時のレスポンスのstatusCodeをいれます。messageはサーバーからのresponse bodyや後ほどsagaで入れ込むために型をもっておきます。
ReduxAPIStructがあるReactの世界
このReduxAPIStructがあるとどのようにコンポーネントを構築できるのか。まずAtomic Designを模したものの重要な点として僕たちは
pages
と template
の明確な責務の分離を行いました。例えば、reduxのconnectをするのは
paegs
級のコンポーネントだけにするなどです。僕たちは
pages
はデータを持ちURLに紐づく単一のページ全体として定義しています。一方
template
はデータを前提につくられたUIとしてつくりました。この明確な責務の分離により
pages
コンポーネントはこのようになります。例えば、以下のようなデータを受け取る記事一覧ページをつくりたいと思います。
データは
{ "items": [ { "title": "タイトル1", "body"; "本文1" }, { "title": "タイトル2", "body"; "本文2" }, { "title": "タイトル3", "body"; "本文3" } ] }
[GET] /items
だとしてaxiosのAPI clientは以下のような実装だとします。import axios from "axios"; class Client { public getItems() { return axios.get("/items"); } }
Promise
を返すように実装します(その方が使う時に楽)sagaのコードは端折りますが、結果を
article reducer
の items
と言うキーにいれるとします。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でも構わないので感想などまたもらえるとすごく嬉しいです。よろしくお願いします。
コメント
コメントを投稿