SYM's Tech Knowledge Index & Creation Records

「INPUT:OUTPUT=1:1以上」を掲げ構築する Tech Knowledge Stack and Index. by SYM@設計者足るため孤軍奮闘する IT Engineer.

React 実践のために調べまくったこと書き綴り まとめメモ

React 実践のために調べまくったこと書き綴り まとめメモ

React のド基礎を押さえただけで React ができるとは言えないので、実践のために以下を行ってみる

そのために調べまくったことを書きなぐった結果故に、偏りがあるのはご容赦を。

フォルダ構成

大事なのは、

  • 1機能を構成する要素(hooks/provider/context 等)は1パッケージにまとめて格納すること(hooks/provider 毎での分散配置はアンチパターン
  • 横断的関心事(共通コンポ/関数や定数等)と機能(features)で閉じる物を明確に分ける
  • 機能(features)で閉じて、依存関係を「機能(features) -> 横断的関心事」とする。このように全体で依存関係が入り乱れないようにする

サンプル

大規模向け。components の下に色々置くでは見通しが悪くなるので、機能ごとに分け、さらに構成要素毎にフォルダを切る。

src
|
+-- assets      # イメージ、フォントなどのすべての静的ファイルを格納
|
+-- components  # 共有コンポーネント
|
+-- config      # グローバル構成、env変数など
|
+-- features    # 機能ベースモジュール ★
|    +--<featureName>
|        +-- api         # エクスポートされたAPIリクエスト宣言と特定機能に関連するAPIフック
|        |
|        +-- components  # 特定機能にスコープした構成部品
|        |
|        +-- hooks       # 特定機能にスコープしたhooks
|        |
|        +-- routes      # 特定機能にスコープしたrouteコンポーネント # イメージ的にはpage兼routingのイメージ
|        |
|        +-- stores      # 特定機能にスコープした状態ストア
|        |
|        +-- types       # 特定機能のドメインの型
|        |
|        +-- utils       # 特定機能のUtility関数
|        |
|        +-- index.ts    # 機能のエントリポイントの場合は、特定の機能のパブリックAPIとして機能し、機能の外部で使用する必要があるすべてのものをエクスポートする。
|
+-- hooks       # 共有フック
|
+-- lib         # 外部ライブラリの再エクスポート
|
+-- providers   # all of the application providers
|
+-- routes      # routes configuration
|
+-- stores      # global state stores
|
+-- test        # test utilities and mock server
|
+-- types       # アプリケーション全体で使用する基本型
|
+-- utils       # 共有Utility関数

小規模~中規模なら、components の下にフォルダを切って格納していく形でもよさそう

小規模で済むなら、公式のような構成でも良さそうだし

多少大きくなるようなら以下のように components を分けるのが良さそう

src
+-- components
|    +-- page  # 1ページを表すコンポーネント (Next.js 使用時)
|    |    +-- top
|    +-- model
|    |    +-- user  # ステート&ロジック?機能?
|    +-- ui
|    ~    +-- button  # ステートを持たないような表示のみのコンポーネント
|    +-- functional
+-- pages  # Next.js使用時 こちらはルーティングのみにする
src/
  ├ components/ ... 汎用的なコンポーネント
  ├ consts/     ... 中央管理したい定数群
  ├ hooks/      ... 汎用的なカスタムフック
  ├ features/
  │   ├ users/  ... ユーザー関連の機能
  │   └ posts/  ... 投稿関連の機能
  └ concerns/       ... 全体を横断して使われる、技術者視点で抜き出した個々の機能
      ├ auth/   ... ページや操作の許可・不許可
      └ toast/  ... トースト関連
          ├ Toast.tsx
          ├ toastContext.tsx
          ├ ToastProvider.tsx
          └ useToast.ts
  • ※ Toast コンポーネントの Props と、状態管理の型を別々に切り離し、Toast.tsx だけ components/ 以下に移動するのもありかもらしい

規模に合わせて、段階的に?フォルダ構成を変えるのが良い?。以下も大規模になった際は、features フォルダを導入している。

refs:

その他

  • フォルダはパスカルケース

    コンポーネントベースの UI ライブラリを使用するフロントエンドでは、コンポーネントを含むフォルダー/ファイルの場合、この名前付け規則が PascalCase に変更されました。

  • pages フォルダは 複数ページ構成なら設けた方が良さそう(pages は Next.js での慣習)

まとめ

上記の総括。src 直下は以下のように分けていくのが良い?(あくまで個人の見解)

この分け方がベストというわけではない。大事なのは、適切に責務/関心等の分離/分類がしやすい形に小さく分ける。管理と把握と拡張をしやすく保つこと。

以下の図のイメージが素晴らしい。

features と components/models の明確な境界がイメージしきれてない(実践してみないと恐らく見えてこない)。悩む&小規模なら統合しても良いかも

src/
  ├ components/          # 共通コンポーネント(特定のfeatureに関係なくまたいで利用されるようなコンポーネント)
  |  ├ pages/              # ページコンポーネント ※features内に入れるべきか不明。個人的にはひとまとめにする場所が欲しい
  |  ├ ui/ or view/        # Viewの部品。見た目に関するコンポーネント
  |  └ models/             # その他。モデルに関するコンポーネント(domain/entityに近いイメージ)
  ├ consts/              # (中央管理したい)共通定数群
  ├ configs/             # グローバル構成、env変数など (constsと一緒でもいいかも)
  ├ hooks/               # 共通カスタムフック
  ├ features/            # 機能ベースコンポーネント群(機能毎でフォルダを割った方がいいなら割る)
  |  └ <FeatureName>/    # ※以下は大規模でなければ分けずにファイル名管理でも十分ならそれで良いと考える
  |    ├ api/              # APIフック
  |    ├ components/
  |    ├ hooks/
  |    ├ routes/ or pages/
  |    ├ stores/ or contexts/
  |    ├ types/
  |    ├ test/
  |    ├ utils/
  |    └ index.js or index.ts
  ├ concerns/            # 横断的に使用する機能コンポーネント群
  ├ providers/           # アプリケーション(全体にかかわる)プロバイダー
  ├ routes/              # ルーティング設定
  ├ stores/ or contexts/ # グローバルストア (Reactのcontextのみならcontextの方が良いかも?)
  ├ libs/ or external/   # 外部ライブラリをラップしたもの
  ├ utils/ or services/  # 共通Utility関数群(functinalでも良いかも? ここは人/組織で命名がブレる)
  ├ types/               # 全体で共有する型定義
  └ test                 # test utilities and mock server

ベストプラクティス(コーディング)

ref: React ベストプラクティスの宝庫!「bulletproof-react」が勉強になりすぎる件

  • アプリケーション内に複数のレンダリング関数を追加しない。(これはすぐに制御できなくなる)
  • ユニットと見なすことができる UI の一部とみなせるなら、それを別のコンポーネントに抽出。

Bad

// this is very difficult to maintain as soon as the component starts growing
function Component() {
  function renderItems() {
    return <ul>...</ul>;
  }
  return <div>{renderItems()}</div>;
}

Good

function Items() {
  return <ul>...</ul>;
}

function Component() {
  return (
    <div>
      <Items />
    </div>
  );
}
  • コンポーネントが受け入れているプロップが多すぎる場合は、複数のコンポーネントに分割するか、子またはスロットを介して合成手法を使用することを検討する。

example code

import { Dialog, DialogTitle } from '@/components/Elements/Dialog';
// :

export type ConfirmationDialogProps = {
  triggerButton: React.ReactElement;
  confirmButton: React.ReactElement;
  // :
}
export const ConfirmationDialog = ({
  triggerButton,
  confirmButton,
  // :
}: (ConfirmationDialogProps) => {
  // :
  const trigger = React.cloneElement(triggerButton, {
    onClick: open,
  });
  return (
    <>
       {trigger}
       // :
       <div>
         {confirmButton}
       </div>
    </>
  );
}

大規模なプロジェクトでは、すべての共有コンポーネントを抽象化することを推奨。これにより、アプリケーションの一貫性が向上し、保守が容易になる。

  • その他

refs:

CSS (Style)

命名は BEM で書くより rscss の方が良い(コンポーネントでクローズドにできるため BEM は冗長)

refs:

styled-components vs emotion

  • 可読性に関して、emotion > styled-components は同意。
  • 速度も emotion 優位らしい。
  • emotion は特定のフレームワークにロックインされない。
  • emotion なら Object Style で書ける。
  • emotion の欠点は SSR 環境への導入はややこしいらしい。

refs:

CSS Modules & CSS-in-JS 特徴

  • パフォーマンスが重要なアプリケーションの場合は CSS Modules の方が適している。
  • CSS-in-JS は、コードの可読性を最大化し、開発者の生産性と開発エクスペリエンスを向上させる。
  • ※パフォーマンス面で CSS-in-JS が最適でないのを解消しようとしているのが ゼロランタイム CSS-in-JS。

ref:

個人の見解

  • 動的に style 変える必要ないのなら、CSS in JS の使用はオーバースペックに思う
  • webpack の css-loader が deprecated にしたため CSS Modules は下火になりつつある?
  • 2022 の結果が出ないと分からないが、満足度:CSS Modules > CSS in JS (Vanilla-Extract 除く) にも関わらず早々消えるとは思えない
  • コンポーネントのソースファイルに CSS の Style の定義が入ってると可読性が良くないように感じる(色々な物が1ファイルに書かれていると何が何かパッと見見づらい)
    • 故に分けたいため、CSS Modules と Vanilla-Extract が個人的には好み(ただ Vanilla-Extract はまだマイナーなのが気になる)
    • だが、ビューとアプリケーションロジックを分ける container/presentation パターン で実装するなら CSS-in-JS でも気にならない
      • ビュー(HTML/CSS メイン)と ロジック(JS の実装ゴリゴリ) で分けれるなら、そちらの方が関心の分離になるため(可読性や責務的にも)良い
  • webpack で CSS Modules が非推奨というだけで、Vite では非推奨になっていない(デフォルトで使用可能)なため、Vite で CSS Modules 使う分には直近問題にはならない

結論(Vite を使用する前提で)

  • 小規模で動的に style 変える必要もないなら CSS Modules で十分
  • 中~大規模で、ビューとアプリケーションロジックを分けてコンポーネントを作ることを徹底するなら CSS-in-JS の方が良いように思う(CSS-in-JS の方が対応の幅が効くというのはありそうなので、先を見据えて CSS-in-JS 採用はありと思う)
  • CSS-is-JS で個人的に使用するなら emotion (ただし、求められるパフォーマンスがシビアでなければ。)

emotion

参考:

<div
  css={[
    css`color: white;`,
    // 1) テーマを引数に取る指定も含められる
    (theme) => css`background-color: ${theme.colors.primary};`,
    // 2) && etc. による条件付き指定も可
    selected && css`background-color: red;`,
  ]}
>

theme の モードチェンジ

refs:

※ sass で実現する方法もある。ちょっと面倒そう

React で CSS variables でダークモードとライトモードを手軽に切り替える

storybook

ref: Vite + React + TypeScript に Vite 用 Storybook を導入する。Storybook は必要だぞ

CSF v3.0

refs:

Template.bind ではなく Object で定義できるようになった

CSF2.0

import { Story, Meta } from "@storybook/react/types-6-0";
import SimpleFrom, { Props } from "./SimpleForm"; // 対象コンポーネント
export default {
  title: "Atoms/SimpleFrom", // CSF3.0では省略可能
  component: SimpleFrom,
} as Meta<Props>;

const Template: Story<Props> = (args) => <SimpleFrom {...args} />;

export const Index = Template.bind({});
Index.args = {
  title: "お名前フォーム",
};

CSF3.0

import { ComponentMeta, ComponentStoryObj } from "@storybook/react";
import SimpleForm from "./SimpleForm"; // 対象コンポーネント
export default { component: SimpleForm } as ComponentMeta<typeof SimpleForm>;

export const Index: ComponentStoryObj<typeof SimpleForm> = {
  args: {
    title: "お名前フォーム",
  },
};
import userEvent from '@testing-library/user-event';

export default { component: AccountForm }

export const Empty = {};

export const EmptyError = {
  ...Empty,
  play: () => userEvent.click(screen.getByText('Submit'));
}

export const Filled = {
  ...Empty,
  play: () => {
    userEvent.type(screen.getById('user'), 'shilman@example.com');
    userEvent.type(screen.getById('password'), 'blahblahblah');
  }
}

export const FilledSuccess = {
  ...Filled,  // 再利用も可能
  play: () => {
    Filled.play();
    EmptyError.play();
  }
}

Interaction test

refs:

以下を導入すれば、再生と巻き戻しも可能

npm i -D @storybook/addon-interactions

.storybook/main.js

  addons: [
    // 他のアドオン,
    '@storybook/addon-interactions'
  ],
  features: {
    interactionsDebugger: true
  },
  • play で実行
  • useEvent で コンポーネントとの相互作用をシミュレート
  • Jest で Dom Structure を検証
// テスト実行
npm run test-storybook

React 諸々

※公式ドキュメントを見ろ。目に入ったものだけを書き留めておく。

ref: React 18 に備えるにはどうすればいいの? 5 分で理解する

Suspense

コンポーネントを「ローディング中なのでまだレンダリングできない」という状態にすることができる

カスタム hooks

  • カスタムフック=ロジックの外だし
  • カスタムフックによるある種のカプセル化が可能な点(現代ではコンポーネントのロジックがほとんどカスタムフックに書かれる)
  • 関数のスコープやモジュールレベルのスコープを活用した多様なカプセル化ができる点が優れている

代表例は、API の実行&レスポンスんの解析 => カスタムフック

refs:

Lazy

ref: React のベストプラクティスとコード削減パターン - パート 1: 4.コードの分割

const Home = React.lazy(() => import("./Home"));
const About = React.lazy(() => import("./About"));
function MyComponent() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Route path="/home" component={Home} />
      <Route path="/about" component={About} />
    </Suspense>
  );
}

ref: named imports for React.lazy

function lazyImport<
  T extends React.ComponentType<any>,
  I extends { [K2 in K]: T },
  K extends keyof I
>(factory: () => Promise<I>, name: K): I {
  return Object.create({
    [name]: React.lazy(() =>
      factory().then((module) => ({ default: module[name] }))
    ),
  });
}

// Usage
const { Home } = lazyImport(() => import("./Home.component.tsx"), "Home");

以下 router と組み合わせて使う

React Router

npm i react-router-dom
npm i -D @types/react-router-dom

React Router Doc: Quick Start

Router の種類(一部のみ。ドキュメント見るべし)

ref: ルーティングライブラリ、React Router(v5)入門

以下のように、コンポーネント化も可能(v6 ~?)

ref: https://github.com/alan2207/bulletproof-react/tree/master/src/routes

export const publicRoutes = [
  {
    path: "/auth/*",
    element: <AuthRoutes />,
  },
];

export const protectedRoutes = [
  {
    path: "/app",
    element: <App />,
    children: [
      { path: "/discussions/*", element: <DiscussionsRoutes /> },
      { path: "/users", element: <Users /> },
      { path: "/profile", element: <Profile /> },
      { path: "/", element: <Dashboard /> },
      { path: "*", element: <Navigate to="." /> },
    ],
  },
];

export const AppRoutes = () => {
  const auth = useAuth();
  const commonRoutes = [{ path: "/", element: <Landing /> }];
  const routes = auth.user ? protectedRoutes : publicRoutes;
  const element = useRoutes([...routes, ...commonRoutes]);
  return <>{element}</>;
};

State 戦略

ref:

3 種

  • サーバーデータのキャッシュ
  • Global State
    • ページをまたいで保持し続ける必要のある State ⇒ Redux や Recoil で管理
  • Local State
    • 各 Component 内で useState を使って管理

デザインパターン

フロントエンドのデザインパターン

その他やって得そうなこと

ref:

以下

  • hooks 抜き出し
  • useMemo/useCallback の適用
    • 「React は Immutability を前提に考えられている世界なので、意味合い的にはメモ化して内容が変化したときにだけ参照も更新されるようにしておいたほうが正しい」
    • 「いざ重くなったときにひとつひとつメモ化からやっていくのは大変だしデグレが怖い」とのこと ⇒ たしかに…。最初からそれなりにやっていった方が良さそう
  • container/presenter の分離

ref:

ビューをアプリケーションロジックから分離(関心の分離を促進)

  • プレゼンテーションコンポーネント
    • 役割:props を通じてデータを受け取り、受け取ったデータを変更することなく、スタイルも含めて意図通りに表示すること
    • プレゼンテーションコンポーネントは(UI のためにステートが必要な場合を除き)ステートを持たない
  • コンテナコンポーネント

Pros:

  • プレゼンテーションコンポーネント
    • UI を担当する純粋関数(=テストが容易)
    • 再利用が容易(アプリケーション全体で異なる目的のために再利用可能=全体での一貫性を保てる)
    • (デザイナーとの) 分業化容易 Cons:
  • 小規模なアプリケーションでは過剰=複雑化

多くの場合、コンテナ・プレゼンテーションパターンは React のフックに置き換えることができる(コンテナ=カスタムフック)

テスト

React Doc: テストのレシピ集

Redux/Recoil

共通点:パフォーマンス上の課題を解決するもの

refs:

React 単体での共通ステート管理の限界

  • コンテキストで、Redux っぽく「全てのステートを詰め込んだオブジェクトでステートを管理し、一つのコンテキストにそのオブジェクトを流す」ように、一つのコンテキストに全ての情報を入れてしまう場合はそれに依存する全てのコンポーネントが再レンダリングされ、パフォーマンス劣化に繋がるため、パフォーマンスを重視する場合に問題となる。

Redux

  • ステートの宣言が中央集権的
  • アプリケーションの状態とロジックを一元化したストアを提供する。
  • コンポーネントが必要なデータに直接アクセスできるようにし、クライアントサーバー環境とネイティブ環境で一貫して動作する JavaScript アプリの作成を支援するように設計された予測可能な状態コンテナー
  • アプリケーション全体でデータの一貫性を提供することにより、複雑なアプリケーションの構築を容易にすることができる
  • 開発者のベストプラクティスを実施するのに優れており、従うべき単純なパターンを提供するため、機能を構築するときにあまり多くの実装決定を行う必要がない
  • API リクエストをアクションに抽象化して、すべてを 1 か所にまとめることもできる
  • Redux Devtools を使用して、アプリケーションの状態がいつ、どこで、なぜ、どのように変化するかを追跡可能
  • React アプリケーションの状態を処理するために提案された 「アーキテクチャ」 である Flux も実装している。(Flux:https://facebook.github.io/flux/

Recoil

  • Recoil はステートの宣言が局所的
  • 大きなメリットは、 React の Suspense との統合
  • フックとの相性がよい(Recoil が提供する各種のフックは、カスタムフックに組み込みやすいように作られている)
  • セレクターで非同期リクエストを作成して、API リクエストから状態を開始することができる
  • (Recoil は React の最新モードと統合されているため)、Suspense を使用してロード状態を処理し、ErrorBoundary を使用してエラーを処理することで、より宣言的な方法でアプリケーションを構築することができる。
  • 開発者エクスペリエンスとユーザーエクスペリエンスの両方に関しては Redux より適している

その他

PLOP

PLOP:テンプレートからファイル生成に使える

以下でも利用している。参考になるはず

github: bulletproof-react

eslint での 依存関係チェック

依存関係のチェック(strict-dependencies)