🛡️

本気で考えるReactのベストプラクティス!bulletproof-react2022

2022/10/10に公開約22,000字4件のコメント

https://zenn.dev/meijin/articles/bulletproof-react-is-best-architecture

と言えば、zennで一番人気のあるの記事です。
Reactの堅牢な開発基盤を築きたいときに非常に参考になります。

@Meijin_gardenさんのこの記事が出たのは、ほぼ一年前。
今日までの間に、React v18.0のリリースというビッグなニュースもありました。
なので、2022年秋となると、一年前とは少し様子も変わってくるかもしれません。
「概ね賛成だけど、ここはこうしてみたい気もする」という部分もあります。

そんなわけで今回は、2022年秋バージョンのbulletproof-reactを一緒に考えていきたいです。

Reactベストプラクティスの宝庫!「bulletproof-react」が勉強になりすぎる件の内容を前提に書いていきますので、まだ読まれていない方は、先に本家の記事を読まれると良さそうです。

改めて考えるbulletproof-reactの良さ

アトミックデザインじゃないの?

Reactの堅牢な開発基盤を整えようとするとき、まず最初に検討したいのが、ディレクトリ構成です。

そして、bulletproof-reactのディレクトリ構成の注目すべきところは、Atomic Designを採用していないという点かもしれません。

Atomic Designとは、化学のメタファーを用いならがら、Atoms(原子)、Molecules(分子)、Organisms(有機体)、Templates(テンプレート)、Pages(ページ)というように、コンポーネントをレイヤーごとに分けて構成する設計システムです。

Atomic Designを用いるとディレクトリ構造は、次のようになります。

├── node_modedules
└── src
       ├── presentational
       │     ├── atoms
       │     │     └── button.tsx
       │     ├── molecules
       │     │     └── serchbox.jsx
       │     └── organisms
       │            └── header.tsx
       └── container
              ├── templates
              │     ├── blogPosts.tsx
              │     └── contact.tsx
              └── pages
                     ├── blogPosts.tsx
                     └── contact.jsx

システマティックで美しい構成です。
しかし、このAtomic Designですが、調べてみると「Atomic Designやめた」系の記事がたくさんヒットします。

こうした記事は、Atomic Designの問題として「どれがMoleculesで、どれがOrganismsなのか人によって解釈が異なる」という点を挙げています。
その一方で、「こうすればAtomic Designでうまくいく」という方法を書いた記事もあります。

記事を読み込みながらチームで議論を進めていけば、分け方の基準も次第に揃っていき、Atomic Designがうまく回り出しそうです。

しかし、依然として、ツッコミどころは残るかもしれません。
例えば、Moleculesの定義として

いくつかのコンポーネントを組み合わせて構成されるような Web UI の知識 (機能) を持つ

とありますが、他のコンポーネントと組み合わさるかもしれないし、組み合わさらないかもしれないコンポーネントはどこに入れたらいいでしょうか。
propsとしてiconを受け取ることが可能なButtonは、Atomsでしょうか、それともMoleculesでしょうか。
悩ましいですね。

一方では、「Moleculesは特定のプロダクトについての知識を持たないもので、Organismsは特定のプロダクトについての知識を持つものだ」という明快な説明もあります。

しかし、もしコンポーネントの粒度ではなく特定のプロダクトの知識を持つか持たないかを基準にするならば、「初めから化学のメタファーを用いる必要はなかったのでは...?」という気もしてしまいます。

確かに、Atomic Designには一定のメリットがあると思います。
バックエンドで当たり前に存在するレイヤー分けの思想が、Atomic Design以前のフロントエンドにはちゃんとしたものがなかったわけですし、ページという単位に縛られるのではなく、パーツを組み立てることによってWebページを構築しようというのは、Atomic Designに限らず大事なことだと思います。

とはいえ、2022年秋。
もう少しシンプルでわかりやすいレイヤー分けも提案されています。

例えば、次の記事です。

https://zenn.dev/yoshiko/articles/99f8047555f700

この記事については、Cybozuさんのイベントで@yoshiko_pgさんご本人からの解説もあって興味深かったです。

https://youtu.be/Rp6yNsHgxhY?t=2461

modelというレイヤーを設けたことが面白いポイントです。
これは、既出の@takepepeさんのAtomicDesign 境界線のひき方に出てくる限定的コンポーネントという考え方にも通ずるものがあります。

横断的コンポーネント限定的コンポーネントの定義は、(解釈が間違っていなければ)以下のようになります。

横断的コンポーネント
特定のコンポーネントディレクトリに隠蔽されていないコンポーネント。
プロジェクト内で横断的に再利用される可能性が高い(関心範囲が広い)もの

components/organisms/ArticlesList
components/molecules/ArticlesListCard
components/atoms/ArticlesButton

限定的コンポーネント
特定のコンポーネントディレクトリに隠蔽されたコンポーネント。
プロジェクト内で横断的に再利用される可能性が低い(関心範囲が狭い)もの

components/templates/Articles/List
components/templates/Articles/List/Card
components/templates/Articles/Button

こうしてAtomic Designの境界線のひき方との共通項を探していくと、@yoshiko_pgさんのディレクトリ構成は、「脱AtomicDesign」というよりも、「ポストAtomicDesign」と呼ぶこともできなくはないかもしれません。

ただ、化学のメタファーを使わないことによって、むしろわかりやすくなったようにも感じます。

そして、そんな新しいレイヤー分けのもう一つの例として、ようやく話は戻ってきますが、bulletproof-reactのディレクトリ構成です。

bulletproof-reactのディレクトリ構成

bulletproof-reactのディレクトリ構成に関する説明は、こちら↓です。

https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md

以下、拙訳のbulletproof-reactからの引用です。

src
├── assets            # 画像やフォントなどの静的ファイル
├── components        # アプリケーション全体で使用できる共通コンポーネント
├── config            # 環境変数などをエクスポートするところ
├── features          # 機能ベースモジュール
├── hooks             # アプリケーション全体で使用できる共通hooks
├── lib               # ライブラリをアプリケーション用に設定して再度エクスポートしたもの
├── providers         # アプリケーションのすべてのプロバイダー
├── routes            # ルーティングの設定
├── stores            # グローバルステートストア
├── test              # テストユーティリティとモックサーバ
├── types             # アプリケーション全体で使用される基本的な型の定義
└── utils             # 共通のユーティリティ関数

(引用ここまで)

assets, hooks, providers, routes, stores, test, types, utilsの説明は割愛します。
それ以外を説明していきます。

config

config配下には以下のようなファイルが入っています。

/config/index.ts
export const API_URL = process.env.REACT_APP_API_URL as string;

bulletproof-reactでは、直接envファイルから環境変数を使うようなことはしないで、一旦このconfigファイルを挟んで、環境変数を使用しています。
こうすることで、環境変数を使う側のファイルにおいて、process.env.REACT_APP_API_URL as stringではなくて、API_URLというシンプルな記述で済みます。
環境変数の設定のミスを発見しやすいメリットもありそうです。

lib

lib配下には以下のようなファイルが入っています。

/lib/index.ts
import Axios, { AxiosRequestConfig } from 'axios';
import { API_URL } from '@/config';

export const axios = Axios.create({
  baseURL: API_URL,
});

これをしないで、ライブラリから直接axiosを使うと、

import { axios } from 'axios';
import { API_URL } from '@/config';

export const fetchUsers = axios.get(`${API_URL}/user`, { params: { ID: 12345 }})

となりますが、libで設定しておいたaxiosを使うと、

import { axios } from '@/lib';

export const fetchUsers = axios.get("/user", { params: { ID: 12345 }})

となり、ちょっとだけコードがスッキリします。

この例だと大差なくてメリット感じづらいかもしれないので、以下にyupの例も書いてみました。

yupの例
import * as yup from 'yup';

const userValidationSchema = yup.object().shape(
  {
    username: yup.string().required("ユーザー名は必須入力です。").max(255, "255文字以内で入力してください。"),
    email: yup.string().email("メールアドレスが不正です。")
    profile: yup.string().min(10, "10文字以上で入力してください。") .max(200, "200文字以下で入力してください。")
  },
)

と、いちいち個別にバリデーションの定義をするのではなくて、

lib/yup.ts
import * as yupDefault from "yup"
import { DateLocale, MixedLocale, NumberLocale, StringLocale } from "yup/lib/locale"

const stringLocale: StringLocale = {
  length: ({ length }) => `${length}文字で入力してください。`,
  min: ({ min }) => `${min}文字以上で入力してください。`,
  max: ({ max }) => `${max}文字以下で入力してください。`,
  email: "正しいメールアドレスを入力してください。",
}

const mixedLocale: MixedLocale = {
  required: ({ label }) => `${label}は必須入力です。`,
}

yupDefault.setLocale({
  string: stringLocale,
})

export const yup = yupDefault

という日本語のエラーメッセージの設定を済ませたファイルをエクスポートしておきさえすれば、どこでも

User.tsx
import { yup } from '@/lib/yup';

const userValidationSchema = yup.object().shape(
  {
    username: yup.label("ユーザー名").string().required().max(255),
    email: yup.label("メールアドレス")..string().email()
    profile: yup.label("プロフィール").string().min(10) .max(200)
  },
)

ように書けるので、ラクチンではないでしょうか。
この方がエラーメッセージの一貫性も担保されるし、良さそうな設計に思えます。

componentsとfeatures

話が逸れました。。。
アトミックデザインの話でしたね。
bulletproof-reactでは、UIに関わる部分は、componentsfeaturesに大きく分かれています。
それぞれ、先ほど出てきた横断的コンポーネント限定的コンポーネントに近しいものです。
@yoshiko_pgさんのディレクトリ構成と比較すると、componentsuiに、featuresmodelsに相当するかと思います。

「最低限のレイヤー分け」という感じがします。
しかし、レイヤーの基準が曖昧になったままAtomic Designを運用していくよりは、これくらいシンプルな構成の方が良いのではないでしょうか。

現状Atomic Designでうまくいっているのなら、敢えてbulletproof-reactのディレクトリ構成に換える必要は全然ないと思います。
もしチームがAtomic Designに疲弊している場合は、bulletproof-reactのシンプルな構成をぜひ試してみてください。

Routingについて

このbulletproof-react、Routing周りが素晴らしいです。

https://github.com/alan2207/bulletproof-react/blob/master/src/routes/protected.tsx

まず、このlazyImportというutility関数は、React.lazyをnamed-exportで使えるようにしたもののようです。
ファイルの冒頭でSuspenseがimportされていますが、いわゆるSuspense For DataFetchingではなく、Suspense For CodeSplittingの用途です。
ここでいうCodeSplitting(コード分割)とは次のようなものです。
以下、Reactの公式ドキュメントから引用します。

コード分割

バンドルは確かに素晴らしいですが、アプリが大きくなるにつれて、バンドルのサイズも大きくなります。
特にサイズの大きなサードパーティ製のライブラリを含む場合は顕著にサイズが増大します。
不用意に大きなバンドルを作成してしまいアプリの読み込みに多くの時間がかかってしまうという事態にならないためにも、常に注意を払い続けなければなりません。
大きなバンドルを不注意に生成してしまわないように、あらかじめコードを「分割」して問題を回避しましょう。
Code-Splitting は、Webpack、Rollup や Browserify(factor-bundle を使用)などのバンドラによってサポートされている機能であり、
実行時に動的にロードされる複数のバンドルを生成することができます。
コード分割は、ユーザが必要とするコードだけを「遅延読み込み」する手助けとなり、アプリのパフォーマンスを劇的に向上させることができます。
アプリの全体的なコード量を減らすことはできませんが、ユーザが必要としないコードを読み込まなくて済むため、初期ロードの際に読む込むコード量を削減できます。

(引用ここまで)

バンドラーは色々なJSファイルをギュッとまとめてくれます。
しかし、ユーザーがWebページを開いた際に、すべてのページのJSファイルが読み込まれる必要があるかというと、それはかなり疑問です。
ユーザーがWebページを開いたときは、その開いたページで必要なJSファイルが読み込まれさえすればよく、他のJSファイルは、ユーザーがそれを必要とするページに遷移したときに読み込まれれば良いのです。
それを可能にするのが、React.lazyというわけです。

便利そうなReact.lazyですが、ReactRouterのv5以前では、意外と扱いが難しかったです。
例えば、

export const  Dashboard = () => {
  return <Layout>省略</Layout>
}

export const Profile = () => {
  return <Layout>省略</Layout>
}
const { Dashboard } = lazyImport(() => import('@/features/misc'), 'Dashboard');
const { Profile } = lazyImport(() => import('@/features/users'), 'Profile');

const App = () => {
  return (
    <BrowserRouter>
      <Suspense fallback={<Spinner size="xl" />}>
        <Switch>
          <Dashboard />
          <Profile />
        </Switch>
      </Suspense>
    </BrowserRouter>
  );
};

と書くと、Dashboardのページから、Profileのページに遷移したときに、Profileのバンドルファイルが読み込まれるまでの一瞬、レイアウトが画面から消えてしまいます。
v5でも他にやりようはあるのですが、なかなか綺麗なコードになりづらい印象でした。

しかし、bulletproof-reactではReactRouterのv6の機能を巧みに使うことで、FatになりがちなJSのバンドルファイルをチャンキーに分割しながら、スッキリとルーティング周りの設定を書くことができています。

この辺りは、OutletNested Routesの成せる技ですね。
破壊的な変更が多いと叩かれがちなReactRouterのv6ですが、なるほど、こういう便利な側面もあるのかと、非常に勉強になりました。

モックサーバーについて

bulletproof-reactは、mswを使ってモックサーバーを立てています。
mswとは何かについては、拙い記事ですが下記をご覧いただけるとありがたいです🙇‍♂️

https://zenn.dev/t_keshi/articles/ok-ihave-mockapi

開発初期のプロジェクトでdummyData.jsonみたいなファイルを作って開発を進めるというやり方をよく目にします。
しかし、これだと「いざAPIとつなぎ込みをしよう」となったときに、相当な工数がかかります。
その代わりに、最初からmswを使っておくと、まるで本物のAPIがそこにあるかのように開発を進められます。本物のAPIへのつなぎ込みも極めてスムーズです。

小さなスタートアップでは、セミナーや展示会までに、なんとか披露できるようなアプリを用意しなければならないケースが多々あります。
そんなときでも、バックエンドは急ピッチで間に合わせのものを作りたくはないですよね。
この問題もmswを使えば、解決できそうです。

bulletproof-reactでは、mswlocalStorageを使ってデータを半永続化させています。
展示会向けのデモなどの用途なら、これで十分乗り切れると思います。

https://github.com/alan2207/bulletproof-react/blob/master/src/test/server/db.ts

モックで作ったアプリの展示会での評判がよければ、しっかりとしたAPIを作り込んでいく..、そんなアジャイルな開発スタイルも取れそうです。

以下、bulletproof-reactのテストの章を拙訳したものです。

APIのプロトタイピングにはmswを使用します。
mswは、サーバーを気にせずにフロントエンドをすばやく作成するための優れたツールです。
これは実際のバックエンドではなく、サービスワーカー内のモックサーバーです。
すべてのHTTPリクエストを傍受し、定義したハンドラに基づいて任意のレスポンスを返します。
これは、バックエンドに機能が実装されていないことによって作業が阻まれていて、フロントエンドだけしかアクセスできないときに特に有効です。
この方法では、機能が完成するのを待ったり、レスポンスデータをハードコーディングしたりする必要はなく、実際のHTTPコールを使ってフロントエンドの機能を構築することができます。

普通にテストするときに使うのも便利ですね。

HTTPClientについて

bulletproof-reactで使用されているHTTTPClientはaxiosです。
axiosと言えば、先日、遂に初のメジャーバージョンがリリースされましたね。

v1.0.0の変更点については@s_sasaki_0529さんが丁寧に解説してくださっています。

https://zenn.dev/sa2knight/articles/axios-major-version

このaxios、一時期は、そろそろやべぇんじゃねぇかと騒がれていたりもしました。

https://github.com/axios/axios/issues/3930

ですが、このメジャーリリースを見ると、axiosが再び息を吹き返した印象です。
この辺りの動向を@wafuwafu13_さんが詳しく解説してくださっている動画がこちら↓です。

https://youtu.be/Rp6yNsHgxhY?t=2461

これを聴く限りだとXianming Zhongという方がaxiosの復活の功労者で、この方がコラボレーターに加わったことでV字回復したという事情があるようです。

他のHTTTPClientを考えると、fetchでは少し物足りない部分がありますし、kyはまだ知名度が低すぎます(良いライブラリなのですが)。
ということで、2022年はaxios続投で良いのではないかと思います。

データフェッチングライブラリについて

bulletproof-reactで使われているデータフェッチングライブラリは、ReactQueryです。
データフェッチングライブラリとは何かについては、拙い記事ですが下記をご覧いただけるとありがたいです🙇‍♂️

https://zenn.dev/t_keshi/articles/react-query-prescription

データフェッチングライブラリには、同じくSWRというものもあります。

https://zenn.dev/uttk/articles/b3bcbedbc1fd00

Next.jsを使うときはSWRの方が相性が良いかもしれませんが、create-react-appviteを使うときはReactQueryの方が何かと便利だと個人的には思ったりします。
両者の違いは、ReactQueryの方は「こっちでありがちなユースケースは全部カバーしといてあげたよ!」という感じで、SWRの方は「最低限のことはこっちでやっとくからあとはそっちでよろしく!」という感じです。
比較検討されている方は、以下の記事が参考になると思います。

https://zenn.dev/terrierscript/articles/2020-07-28-swr-react-query

それってどうなの?、bulletproof-react

ここまでで、bulletproof-reactの良い点を挙げてきました。
ここからは、bulletproof-reactの個人的に「あれ、それってどうなの?」と思うところを挙げてみたいと思います。

Reactの開発環境について

bulletproof-reactcreate-react-app(CRA)を使用しています。
「Reactの開発環境と言えばCRA」
今まではそれが当たり前だったかもしれません。

ですが、CRAにはいくつか問題があります。
問題点を@er11161さんのビルドシステムをcreate-react-appからViteに移行した話から引用します。

コードベースが大きくなるにつれてビルドにかかる時間が増加し、開発サーバーの起動やHot Module Replacementに時間がかかってしまう。

わかりみです。
私も現在、本業ではCRAを使って開発しているのですが、npm run startしてから、アプリが立ち上がるまでの間に、コーヒーを買いに行って戻ってこれるくらいの時間がかかってしまっております。

webpack configのカスタマイズがしづらく、ビルドの高速化やチャンク分割に向き合いづらい

CRAwebpackの設定に手を入れるのは複雑になりがちです。
react-app-rewiredとかcracoを使うことになると思うのですが、にしても単純明快にはいかない。bulletproof-reactでもcracoが使われていますが、もうちょっとシンプルにできたら良さそうです。

そんな中、Vue.jsの作者であるEvan You氏が開発したViteが注目を集めています。
私も少し試してみたところ、「一度このスピードに慣れてしまうと二度とCRAには戻れないな」という感想でした。

速いのはアプリの立ち上がりだけではなく、テストもです。
Viteのテスト、Vitestは、Jestと比較しても2倍以上早いようです。
(参考: Vitest はどれくらい早いのか ~ Jest と比較 ~)
公式サイトにJest Compatibleと書いてある通り、書き味はJestと同じです。

このテーマについてより詳細は、以下のissueを追ってみるのが良さそうです。

https://github.com/facebook/create-react-app/issues/11180

Dan先生がReact本家の開発に忙しく、CRAにあまり時間を割けないという事情も察せられます。
それもあってか、1ヶ月の間に閉じられているPRの数はCRAが5に対し、Viteは70。
実に、14倍です。

ということで、2022年、本命はやはりViteではないでしょうか。
また、@sykmhmhさんの記事も非常に参考になったので、下記に貼らせてください🙇‍♂️

https://zenn.dev/sykmhmh/articles/ff09bea2cf7026

Next.jsについて

Next.jsは、今とても勢いがあります。

Rust製のコンパイラは高速です。
APIRoutesは、お手軽にAPIが作れて便利です。
SEOにも強いです。

私もNext.jsが好きで、個人開発するときにはよく使っています。
ですが、どんなプロジェクトでも必ずNext.jsを使うべきだとは思いません。
Next.jsだと動作しないライブラリや、windowundefinedだった場合の処理を書かなければならないことなど、Next.jsであるが故に困ることもしばしばあるからです。

それを加味すると、Next.jsの採用の決め手はやはり「サーチエンジンの検索結果で上位にくる必要があるかどうか」ではないでしょうか。

Rust製のコンパイラは速いですが、ViteのGolang製のコンパイラも負けじと速いです。
APIRoutesは便利ですが、同じJSでちゃんと作り込むならやはりNestJSなどを使った方がいいと思います。

CSSフレームワークについて

bulletproof-reactは、Tailwindを採用しています。

Tailwindと言えば、先日@uhyoさんが「Tailwind考」を書かれて、界隈でも話題になりました。

https://blog.uhy.ooo/entry/2022-10-01/tailwind

いきなり序盤から、

筆者が一番みなさんに伝えたいことは、Tailwind CSSは考え無しに採用してよい技術ではなく、採用するには熟慮が必要だということです。
とくに、フロントエンドのスターターキット的なプロジェクトの中にTailwind CSSが混ざっていることがありますが、あれはけっこうな罠です。
気軽に採用すべきものではありません。

とあって、なんだかbulletproof-reactのことを指しているような気もしました。

最近は、IDEも進化してコード補完も効きくので、CSSが短く書けるから速く書けるということにはなりません。
uhyoさんの記事を読む限りでは、Tailwindを採用するメリットはあまり大きくないのではないかと感じます。

ところで、bulletproof-reactでは、パフォーマンスへの配慮という観点でTailwindを使用しているようです。
Perfonmanceの章では、次のように書かれています(拙訳)。

アプリケーションがパフォーマンスに影響を与える可能性のある頻繁な更新が予想される場合は、実行時のスタイリング ソリューション ( 実行時にスタイルを生成するChakra UI、emotion、styled-components ) からZero-Runtimeのスタイリング ソリューション ( tailwind、linaria、vanilla-extract、CSS ) に切り替えることを検討してください。

しかし個人的な意見ですが、RuntimeCSSのせいで、アプリケーションがストレスを感じるほど遅くなっているというケースは非常に稀だと感じます。
もしパフォーマンスを向上させようとするのであれば、1番に考えるべきはSQLの最適化ではないでしょうか。

ただやはり、「アプリの動作が高速であること」という要件のプライオリティが高い場合には、Tailwindの採用は検討の余地があるかもしれませんね。

E2Eテストについて

bulletproof-reactでは、E2EのテストのライブラリとしてCypressが使われています。

https://github.com/alan2207/bulletproof-react/blob/master/cypress/integration/smoke.ts

E2Eテストについては、以下の記事で少しだけ触れています。

https://zenn.dev/t_keshi/articles/react-test-rule

今日からはじめるReactのテスト実践の方では、実際にCypress を使ってテストを書くということはしていません。

理由は下記の通りです。
「ユーザーが実際にアプリケーションを使うようなやり方でテストをできる」という点はCypressの大きな魅力で、アプリケーションの品質に対する自信につながります。
しかし、実際にCypressを使ってみるとテストの結果が成功したり失敗したりすることが頻繁にありました。
あまり書きやすくはなかったです。

結合テストはreact-testing-libraryに任せて、E2EテストはAutifyなどのローコードツールを頼るのがエンジニアの負担も少なく、最も良い手なのではないでしょうか。

@riririusei99さんのE2Eフレームワーク比較検討記事も非常に参考になったので、下記に貼らせてください🙇‍♂️

https://teamspirit.hatenablog.com/entry/2020/04/17/150000

状態管理について

最後に、状態管理の話です。
bulletproof-reactは、Zustandを採用しています。

こうした状態管理のライブラリについて、まず考えてみたいのは、
「なぜContextAPIでは駄目なのか?」
ということです。

Tanner Linsley氏の以下の講演では、ContextAPIを用いて快適に状態管理を行う手法が提案されています。

https://www.youtube.com/watch?v=J-g9ZJha8FE

この動画では、不要な再レンダリングに対して、コンテキストを分割する方法を推奨しています。
もう少しシンプルなところで言うと、下記のような方法もあります。

https://github.com/streamich/react-use/blob/master/src/factory/createReducerContext.ts

使用方法はこちらにも書かれていますが、自分なりの例を書かせていただくと、

providers/snackbar.tsx
import { produce } from 'immer';
import { createReducerContext } from 'react-use';

type Severity = "warning" | "error" | "info"

interface State {
  isOpen: boolean;
  severity: Severity | undefined;
  message: string;
}

interface Close {
  type: 'close';
}

interface Open {
  type: 'open';
  payload: {
    severity?: Severity
    message: string;
  };
}

type Action = Close | Open;

const initialState: State = {
  isOpen: false,
  severity: undefined,
  message: '',
};

export const reducer = produce((draft: State, action: Action): void => {
  switch (action.type) {
    case 'close':
      draft.isOpen = initialState.isOpen;
      draft.severity = initialState.severity;
      draft.message = initialState.message;

      return;
    case 'open':
      draft.isOpen = true;
      draft.severity = action.payload.severity;
      draft.message = action.payload.message;

      return;
    default:
      throw new Error('Invalid action passed to snackbar dispatch');
  }
});

export const [useSnackbar, SnackbarProvider] = createReducerContext<typeof reducer>(
  reducer,
  initialState,
);
App.tsx
import { useSnackbar, SnackbarProvider } from '@/providers/snackbar'

const Example = () => {
  const [state, dispatch] = useSnackbar()

  return(
    <div>
      <Snackbar message={state.message} severity={state.severity} />
      <button onClick={() => dispatch({ type: 'close' })} />
    </div>
  )
}

const App = () => {
  return (
    <SnackbarProvider>
      <Example />
    </SnackbarProvider>;
  {}
}

このように比較的簡単に書くことができます。
ちなみにReduxのcombineReducers的なものが欲しければ次のようにすることもできます。

import React from 'react';

const combineProviders: (providers: React.FC[]) => React.FC = (
  providers,
) =>
  providers.reduce((Combined, Provider) => {
    const combine = ({
      children,
    }: React.ComponentProps<React.FC>): JSX.Element => (
      <Combined>
        <Provider>{children}</Provider>
      </Combined>
    );

    return combine;
  });

const Providers: React.FC = combineProviders([SnackbarProvider, HogeProvider, HogeHogeProvider]);

つまり、ContextAPIも少し手を加えれば、十分使いやすくなるのです。

そういうわけで、最初はContextAPIを使用しておいて、どうしても状態管理のライブラリの必要になったタイミングでZustandなどを導入するという方針はいかがでしょうか。

もし、ContextAPIで済むのなら、それに越したことはありません。

それでも、状態管理ライブラリが必要になるとき

上で言いたかったこととしては、「もしuseContextの書き味が気に入らないのであれば、自分の工夫次第で解決できるかも」ということです。
もっとシンプルな書き味がよければ、下記のcreateStateContextも参考になるかもしれません。
https://github.com/streamich/react-use/blob/master/src/factory/createStateContext.ts

書き味の違いを除くと、状態管理ライブラリがどうしても必要になるときというのは、やはりパフォーマンスが気になるときです。
下記は、@uhyoさんの記事Facebook製の新しいステート管理ライブラリ「Recoil」を最速で理解するからの引用です。

RecoilとReduxが共通する点は、パフォーマンス上の課題を解決するものであるという点でした。Recoilの使い方の説明に入る前に、これについて解説します。
パフォーマンス上の課題とは何かというのは、言い換えれば「useReducer + useContextでうまくいかないとのはどういう時か」という問いでもあります。

この記事では、useContext主体の状態管理においてReduxuseSelectorを再現しようとします。
しかし、Contextの場合だとどうしても、useSelectorを使う全てのコンポーネントで再レンダリングを引き起こしてしまいます。
Tanner Linsley氏の動画のようなコンテキストを分割する方法もあるのですが、それでも結論としては、パフォーマンスを本気で考えるなら状態管理のライブラリに一日乃長があります。

ただ、一体どれだけのアプリが本気でパフォーマンスチューニングを考えなければならないステージにあるか、ということも考えたくはなります。
売れているプロダクトなら別ですが、普通、エンジニアがやらなければならないのは、素早く改善要望に応え、機能を追加して..ということです。
パフォーマンスを本気で考えるのは、PMFを果たした後で良いのではないでしょうか。

そういう意味では、プロジェクトの初期段階では、状態管理ライブラリは何も入れないでOKな気がしています。

終わりに

色々書いてきましたが、みんな作っているプロダクトも、事業領域も、メンバーの保有スキルも、全部違います。
話をひっくり返すようですが、個々の置かれた状況の多様性を無視して、ベストな技術スタックは何かと十把一絡げにしている本記事の内容には相当無理があります。
そうではなくて、ウチのチームにぴったりFitする選択肢は何かという方向で、議論していかなくてはと思います。

そして、そうした議論の過程を記録に残しておくことも重要です。
ということで最後に@souppowerさんのArchitecture Decision Records(ADR)に関する記事を貼らせてください🙇‍♂️

https://zenn.dev/souppower/articles/bfdf79069ae9a7

。。。

なんかbulletproof-reactにかこつけて、フロントエンドの雑多なトピックについて、徒然なるままに書いた形になってしまいました。
どちらかというと「2022年のReactの話題まとめ」みたいなタイトルが適切だったかもしれません。

twitterなどで気軽に感想を教えてもらえると嬉しいです。
あくまで素人の意見なので、どうかお手柔らかにお願いします🙏

https://twitter.com/t__keshi

最後までお読みいただきありがとうございました!!

GitHubで編集を提案

Discussion

「なぜContextAPIでは駄目なのか?」というとdispatchだけしてstateには無関心なコンポーネントも再描画されるからではないかと思います。
なのでyoutubeの動画ではstateとdispatchでproviderを分けていると思います。
で、そういうことをしているとproviderが増えまくるなぁということで、何かライブラリ使おうという流れになる気がします。

なるほど〜
確かにproviderが増えまくるのは嫌ですし、両方必要なときに

const Hoge = () => {
    const state = useSnackbarState()
    const dispatch = useSnackbarDispatch()

みたいに2個useContextを呼び出さないといけないのもちょっとダルいですね。
となると、やはりjotaiやrecoilを入れた方が良さそうな気もして気もしきます。
(うーん、悩ましい..🤔)

コメントありがとうございました!

ログインするとコメントできます