背景
toB向けのプロダクトをReactで作っています。
2022年1月に入社してから2月にローンチし、今日まで改修を続けています。
本記事は、アプリケーションが大きくなるにつれて、ディレクトリ構造やコード分割をどのように変えていったかの備忘録になります。
初期(2022年2月)
MVPにむかって最短で走った
src/
├── App.tsx
├── components // Atoms, Molecules単位の要素をアプリで統一
├── constants // 定数定義
├── contexts // Context
├── index.tsx
├── organisms // organismsの定義
├── pages
│ ├── Login.tsx
│ ├── Order.tsx
│ └── OrderList.tsx
├── providers // Provider
└── stories // storybook
資金調達間近(2022年6月)
ここが使いづらいとか、ニッチなエラーとか、メールで報告くださるユーザさんには感謝しかない
src/
├── App.tsx
├── components
├── constants
├── contexts
├── index.tsx
├── pages
│ ├── Contract
│ │ ├── ContractItem.tsx // organismsディレクトリが消え、organisms, templatesは各pageの下に置くことに
│ │ ├── contractUtils.ts // なぜかutilがこんなところにある…
│ │ └── index.tsx
│ ├── Login
│ │ └── index.tsx
│ ├── OrderCreate
│ │ ├── OrderFormComplete.tsx
│ │ ├── OrderFormConditions.tsx
│ │ ├── OrderFormConfirm.tsx
│ │ └── index.tsx
│ ├── OrderDetail
│ │ ├── EstimateItem.tsx
│ │ ├── OrderEdit.tsx
│ │ ├── OrderRequestsContent.tsx
│ │ └── index.tsx
│ └── OrderList
│ ├── OrderCard.tsx
│ ├── index.tsx
│ └── useOrderList.tsx // customHookの片鱗が見える
├── providers
├── stories
├── tests // ようやくテストコードを追加
└── utils // いろんなところで使う便利関数を定義
このあたりでページを構成するファイルが長くなってきたので、Organismsの単位で別ファイルに分離し、見通しやすくした
現在進行系(2022年10月~)
デザインリニューアルも兼ねてアーキテクチャ見直し
src/
├── @types
├── App.tsx
├── components
├── constants
├── contexts
├── hooks // customHookがディレクトリとして独立
├── images // 画像
├── index.tsx
├── pages
│ ├── Billing
│ ├── Contract
│ ├── Login
│ ├── OrderCreate // 他ページの中は省略
│ │ ├── OrderCreateContainer.tsx // Container - Presenterモデルを採用し、よりtestableを目指す
│ │ ├── desktop // for PC
│ │ │ ├── OrderCreate.tsx // Presenter
│ │ │ ├── OrderFormComplete.tsx // ステップ3のorganisms
│ │ │ ├── OrderFormConfirm.tsx // ステップ2のorganisms
│ │ │ └── OrderFormInput.tsx // ステップ1のorganisms
│ │ ├── smartphone
│ │ │ └── 省略
│ │ └── tablet
│ │ └── 省略
│ ├── OrderDetail
│ ├── OrderList
│ ├── OrderUpdate
│ ├── Usage
│ └── UsageDetail
├── providers
├── stories
├── tests
└── utils
Container - Presenterモデルを採用した。
- Containerは、useStateやuseCustomHookを使い、状態と関数を保持する
- Presenterは、Containerから状態と関数を受け取り、ハンドラを登録し、UIを表示する
Presenterがstorybookに登録しやすくなり、ContainerをMock化することでテストも書きやすくなる。
あとレスポンシブ対応をやめて、PC, SP, TableそれぞれのPresenterを用意し、react-device-detectを用いて分岐させた。
dynamic importを使うことで、3つのPresenterにそれぞれ別名をつけなくて済んだし、そもそもユーザのデバイスと違うPresenterをimportしなくていいので気に入っている。
// この分岐毎回書くのだるいよね…どうにかApp側でできないかな〜
const Presenter = React.lazy(() =>
isBrowser
? import("pages/Page/desktop/Presenter")
: isTablet
? import("pages/Page/tablet/Presenter")
: import("pages/Page/smartphone/Presenter")
);
const Container = () => {
const [state, setState] = useState();
const { submit } = useCustomHook();
return (
<Suspense fallback={<Loading />}>
<Presenter
state={state}
setState={setState}
submit={submit}
/>
</Suspense>
);
};
インターン生から提案を受けて以下の変更を試みる予定
styled-componentを用いたJSXの定義をStyleファイルに分離する
src/
├── @types
├── App.tsx
├── components
├── constants
├── contexts
├── hooks
├── images
├── index.tsx
├── pages
│ ├── Billing
│ ├── Contract
│ ├── Login
│ ├── OrderCreate
│ │ ├── OrderCreateContainer.tsx // Container
│ │ ├── desktop // for PC
│ │ │ ├── OrderCreatePresenter.tsx // Presenter
│ │ │ └── OrderCreateStyle.tsx // Preseterで使うJSXの定義
│ │ ├── smartphone
│ │ │ └── 省略
│ │ └── tablet
│ │ └── 省略
│ ├── OrderDetail
│ ├── OrderList
│ ├── OrderUpdate
│ ├── Usage
│ └── UsageDetail
├── providers
├── stories
├── tests
└── utils
JSXの定義を別ファイルに分けることで、マークアップの構成とスタイルの関心を分離できる。
たしかに1ファイルにしているとマークアップとスタイルを行き来して煩わしかったので良さそう。
// XxxStyle.tsx
import styled from "styled-components";
export const Body = styled.div`
width: 920px;
height: 600px;
display: flex;
flex-direction: column;
gap: 12px;
`;
export const Title = styled.h1`
font-size: 28px;
`;
// XxxPresenter.tsx
import Button from "src/components/Button";
import { Body, Title } from "src/pages/xxx/desktop/XxxStyle";
const XxxPresenter = ({
state,
submit,
}: {
state: string;
submig: () => Promise<void> | void;
}) => {
return (
<Body>
<Title>{state}</Title>
<Button
label={state}
onClick={submit}
/>
</Body>
)
};
export default XxxPresenter;
// XxxContainer.tsx
const Presenter = React.lazy(() =>
isBrowser
? import("pages/Page/desktop/XxxPresenter")
: isTablet
? import("pages/Page/tablet/XxxPresenter")
: import("pages/Page/smartphone/XxxPresenter")
);
const XxxContainer = () => {
const [state, setState] = useState();
const { submit } = useCustomHook();
return (
<Suspense fallback={<Loading />}>
<Presenter
state={state}
submit={submit}
/>
</Suspense>
);
};
export default Container;