LoginSignup
15
5

More than 1 year has passed since last update.

Reactのアーキテクチャの変遷

Last updated at Posted at 2022-10-20

背景

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;
15
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
5