俺の考えた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でも構わないので感想などまたもらえるとすごく嬉しいです。よろしくお願いします。
コメント
コメントを投稿