本気で考えるReactのベストプラクティス!bulletproof-react2022
と言えば、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やめた」系の記事がたくさんヒットします。
- Reactのディレクトリ構成でAtomicデザインをやめた話
- Atomic designを辞めて利用目的別のディレクトリ構成に移行する
- Reactでアトミックデザインやめた話
- Atomic Designをやめてディレクトリ構造を見直した話
- 【Atomic Designに懐疑的なあなたへ】改めて考えたい React / Next.js のデザインパターン
- Atomic Design is messy, here's what I prefer
こうした記事は、Atomic Design
の問題として「どれがMoleculesで、どれがOrganismsなのか人によって解釈が異なる」という点を挙げています。
その一方で、「こうすればAtomic Designでうまくいく」という方法を書いた記事もあります。
- AtomicDesign 境界線のひき方
- Atomic Design はなぜ難しいか?どうやって難しさを解消するか
- Atomic Design における Atom, Molecule, Organism の見極め方
記事を読み込みながらチームで議論を進めていけば、分け方の基準も次第に揃っていき、Atomic Design
がうまく回り出しそうです。
しかし、依然として、ツッコミどころは残るかもしれません。
例えば、Molecules
の定義として
いくつかのコンポーネントを組み合わせて構成されるような Web UI の知識 (機能) を持つ
とありますが、他のコンポーネントと組み合わさるかもしれないし、組み合わさらないかもしれないコンポーネントはどこに入れたらいいでしょうか。
propsとしてiconを受け取ることが可能なButton
は、Atoms
でしょうか、それともMolecules
でしょうか。
悩ましいですね。
一方では、「Moleculesは特定のプロダクトについての知識を持たないもので、Organismsは特定のプロダクトについての知識を持つものだ」という明快な説明もあります。
しかし、もしコンポーネントの粒度ではなく特定のプロダクトの知識を持つか持たないかを基準にするならば、「初めから化学のメタファーを用いる必要はなかったのでは...?」という気もしてしまいます。
確かに、Atomic Design
には一定のメリットがあると思います。
バックエンドで当たり前に存在するレイヤー分けの思想が、Atomic Design
以前のフロントエンドにはちゃんとしたものがなかったわけですし、ページという単位に縛られるのではなく、パーツを組み立てることによってWebページを構築しようというのは、Atomic Design
に限らず大事なことだと思います。
とはいえ、2022年秋。
もう少しシンプルでわかりやすいレイヤー分けも提案されています。
例えば、次の記事です。
この記事については、Cybozuさんのイベントで@yoshiko_pgさんご本人からの解説もあって興味深かったです。
YouTubeのvideoIDが不正です
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
のディレクトリ構成に関する説明は、こちら↓です。
以下、拙訳の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配下には以下のようなファイルが入っています。
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配下には以下のようなファイルが入っています。
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文字以下で入力してください。")
},
)
と、いちいち個別にバリデーションの定義をするのではなくて、
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
という日本語のエラーメッセージの設定を済ませたファイルをエクスポートしておきさえすれば、どこでも
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に関わる部分は、components
とfeatures
に大きく分かれています。
それぞれ、先ほど出てきた横断的コンポーネントと限定的コンポーネントに近しいものです。
@yoshiko_pgさんのディレクトリ構成と比較すると、components
がui
に、features
がmodels
に相当するかと思います。
「最低限のレイヤー分け」という感じがします。
しかし、レイヤーの基準が曖昧になったままAtomic Design
を運用していくよりは、これくらいシンプルな構成の方が良いのではないでしょうか。
現状Atomic Design
でうまくいっているのなら、敢えてbulletproof-react
のディレクトリ構成に換える必要は全然ないと思います。
もしチームがAtomic Design
に疲弊している場合は、bulletproof-react
のシンプルな構成をぜひ試してみてください。
Routingについて
このbulletproof-react
、Routing周りが素晴らしいです。
まず、この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のバンドルファイルをチャンキーに分割しながら、スッキリとルーティング周りの設定を書くことができています。
この辺りは、Outlet
とNested Routes
の成せる技ですね。
破壊的な変更が多いと叩かれがちなReactRouterのv6ですが、なるほど、こういう便利な側面もあるのかと、非常に勉強になりました。
モックサーバーについて
bulletproof-react
は、msw
を使ってモックサーバーを立てています。
msw
とは何かについては、拙い記事ですが下記をご覧いただけるとありがたいです🙇♂️
開発初期のプロジェクトでdummyData.json
みたいなファイルを作って開発を進めるというやり方をよく目にします。
しかし、これだと「いざAPIとつなぎ込みをしよう」となったときに、相当な工数がかかります。
その代わりに、最初からmsw
を使っておくと、まるで本物のAPIがそこにあるかのように開発を進められます。本物のAPIへのつなぎ込みも極めてスムーズです。
小さなスタートアップでは、セミナーや展示会までに、なんとか披露できるようなアプリを用意しなければならないケースが多々あります。
そんなときでも、バックエンドは急ピッチで間に合わせのものを作りたくはないですよね。
この問題もmsw
を使えば、解決できそうです。
bulletproof-react
では、msw
とlocalStorage
を使ってデータを半永続化させています。
展示会向けのデモなどの用途なら、これで十分乗り切れると思います。
モックで作ったアプリの展示会での評判がよければ、しっかりとしたAPIを作り込んでいく..、そんなアジャイルな開発スタイルも取れそうです。
以下、bulletproof-reactのテストの章を拙訳したものです。
APIのプロトタイピングには
msw
を使用します。
mswは、サーバーを気にせずにフロントエンドをすばやく作成するための優れたツールです。
これは実際のバックエンドではなく、サービスワーカー内のモックサーバーです。
すべてのHTTPリクエストを傍受し、定義したハンドラに基づいて任意のレスポンスを返します。
これは、バックエンドに機能が実装されていないことによって作業が阻まれていて、フロントエンドだけしかアクセスできないときに特に有効です。
この方法では、機能が完成するのを待ったり、レスポンスデータをハードコーディングしたりする必要はなく、実際のHTTPコールを使ってフロントエンドの機能を構築することができます。
普通にテストするときに使うのも便利ですね。
HTTPClientについて
bulletproof-react
で使用されているHTTTPClientはaxios
です。
axios
と言えば、先日、遂に初のメジャーバージョンがリリースされましたね。
v1.0.0の変更点については@s_sasaki_0529さんが丁寧に解説してくださっています。
このaxios
、一時期は、そろそろやべぇんじゃねぇかと騒がれていたりもしました。
ですが、このメジャーリリースを見ると、axios
が再び息を吹き返した印象です。
この辺りの動向を@wafuwafu13_さんが詳しく解説してくださっている動画がこちら↓です。
YouTubeのvideoIDが不正です
これを聴く限りだとXianming Zhongという方がaxios
の復活の功労者で、この方がコラボレーターに加わったことでV字回復したという事情があるようです。
他のHTTTPClientを考えると、fetch
では少し物足りない部分がありますし、ky
はまだ知名度が低すぎます(良いライブラリなのですが)。
ということで、2022年はaxios
続投で良いのではないかと思います。
データフェッチングライブラリについて
bulletproof-react
で使われているデータフェッチングライブラリは、ReactQuery
です。
データフェッチングライブラリとは何かについては、拙い記事ですが下記をご覧いただけるとありがたいです🙇♂️
データフェッチングライブラリには、同じくSWR
というものもあります。
Next.js
を使うときはSWR
の方が相性が良いかもしれませんが、create-react-app
やvite
を使うときはReactQuery
の方が何かと便利だと個人的には思ったりします。
両者の違いは、ReactQuery
の方は「こっちでありがちなユースケースは全部カバーしといてあげたよ!」という感じで、SWR
の方は「最低限のことはこっちでやっとくからあとはそっちでよろしく!」という感じです。
比較検討されている方は、以下の記事が参考になると思います。
それってどうなの?、bulletproof-react
ここまでで、bulletproof-react
の良い点を挙げてきました。
ここからは、bulletproof-react
の個人的に「あれ、それってどうなの?」と思うところを挙げてみたいと思います。
Reactの開発環境について
bulletproof-react
はcreate-react-app(CRA)
を使用しています。
「Reactの開発環境と言えばCRA」
今まではそれが当たり前だったかもしれません。
ですが、CRA
にはいくつか問題があります。
問題点を@er11161さんのビルドシステムをcreate-react-appからViteに移行した話から引用します。
コードベースが大きくなるにつれてビルドにかかる時間が増加し、開発サーバーの起動やHot Module Replacementに時間がかかってしまう。
わかりみです。
私も現在、本業ではCRA
を使って開発しているのですが、npm run start
してから、アプリが立ち上がるまでの間に、コーヒーを買いに行って戻ってこれるくらいの時間がかかってしまっております。
webpack configのカスタマイズがしづらく、ビルドの高速化やチャンク分割に向き合いづらい
CRA
でwebpack
の設定に手を入れるのは複雑になりがちです。
react-app-rewiredとかcracoを使うことになると思うのですが、にしても単純明快にはいかない。bulletproof-react
でもcraco
が使われていますが、もうちょっとシンプルにできたら良さそうです。
そんな中、Vue.js
の作者であるEvan You
氏が開発したVite
が注目を集めています。
私も少し試してみたところ、「一度このスピードに慣れてしまうと二度とCRAには戻れないな」という感想でした。
速いのはアプリの立ち上がりだけではなく、テストもです。
Vite
のテスト、Vitestは、Jest
と比較しても2倍以上早いようです。
(参考: Vitest はどれくらい早いのか ~ Jest と比較 ~)
公式サイトにJest Compatible
と書いてある通り、書き味はJest
と同じです。
このテーマについてより詳細は、以下のissueを追ってみるのが良さそうです。
Dan先生がReact本家の開発に忙しく、CRA
にあまり時間を割けないという事情も察せられます。
それもあってか、1ヶ月の間に閉じられているPRの数はCRA
が5に対し、Vite
は70。
実に、14倍です。
ということで、2022年、本命はやはりVite
ではないでしょうか。
また、@sykmhmhさんの記事も非常に参考になったので、下記に貼らせてください🙇♂️
Next.jsについて
Next.js
は、今とても勢いがあります。
Rust製のコンパイラは高速です。
APIRoutesは、お手軽にAPIが作れて便利です。
SEOにも強いです。
私もNext.js
が好きで、個人開発するときにはよく使っています。
ですが、どんなプロジェクトでも必ずNext.js
を使うべきだとは思いません。
Next.js
だと動作しないライブラリや、window
がundefined
だった場合の処理を書かなければならないことなど、Next.js
であるが故に困ることもしばしばあるからです。
それを加味すると、Next.js
の採用の決め手はやはり「サーチエンジンの検索結果で上位にくる必要があるかどうか」ではないでしょうか。
Rust製のコンパイラは速いですが、Vite
のGolang製のコンパイラも負けじと速いです。
APIRoutes
は便利ですが、同じJSでちゃんと作り込むならやはりNestJS
などを使った方がいいと思います。
CSSフレームワークについて
bulletproof-react
は、Tailwind
を採用しています。
Tailwind
と言えば、先日@uhyoさんが「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
が使われています。
E2Eテストについては、以下の記事で少しだけ触れています。
今日からはじめるReactのテスト実践の方では、実際にCypress
を使ってテストを書くということはしていません。
理由は下記の通りです。
「ユーザーが実際にアプリケーションを使うようなやり方でテストをできる」という点はCypress
の大きな魅力で、アプリケーションの品質に対する自信につながります。
しかし、実際にCypress
を使ってみるとテストの結果が成功したり失敗したりすることが頻繁にありました。
あまり書きやすくはなかったです。
結合テストはreact-testing-library
に任せて、E2EテストはAutifyなどのローコードツールを頼るのがエンジニアの負担も少なく、最も良い手なのではないでしょうか。
@riririusei99さんのE2Eフレームワーク比較検討記事も非常に参考になったので、下記に貼らせてください🙇♂️
状態管理について
最後に、状態管理の話です。
bulletproof-react
は、Zustandを採用しています。
こうした状態管理のライブラリについて、まず考えてみたいのは、
「なぜContextAPIでは駄目なのか?」
ということです。
Tanner Linsley氏の以下の講演では、ContextAPIを用いて快適に状態管理を行う手法が提案されています。
この動画では、不要な再レンダリングに対して、コンテキストを分割する方法を推奨しています。
もう少しシンプルなところで言うと、下記のような方法もあります。
使用方法はこちらにも書かれていますが、自分なりの例を書かせていただくと、
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,
);
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
も参考になるかもしれません。
書き味の違いを除くと、状態管理ライブラリがどうしても必要になるときというのは、やはりパフォーマンスが気になるときです。
下記は、@uhyoさんの記事Facebook製の新しいステート管理ライブラリ「Recoil」を最速で理解するからの引用です。
RecoilとReduxが共通する点は、パフォーマンス上の課題を解決するものであるという点でした。Recoilの使い方の説明に入る前に、これについて解説します。
パフォーマンス上の課題とは何かというのは、言い換えれば「useReducer + useContextでうまくいかないとのはどういう時か」という問いでもあります。
この記事では、useContext
主体の状態管理においてRedux
のuseSelector
を再現しようとします。
しかし、Contextの場合だとどうしても、useSelector
を使う全てのコンポーネントで再レンダリングを引き起こしてしまいます。
Tanner Linsley氏の動画のようなコンテキストを分割する方法もあるのですが、それでも結論としては、パフォーマンスを本気で考えるなら状態管理のライブラリに一日乃長があります。
ただ、一体どれだけのアプリが本気でパフォーマンスチューニングを考えなければならないステージにあるか、ということも考えたくはなります。
売れているプロダクトなら別ですが、普通、エンジニアがやらなければならないのは、素早く改善要望に応え、機能を追加して..ということです。
パフォーマンスを本気で考えるのは、PMFを果たした後で良いのではないでしょうか。
そういう意味では、プロジェクトの初期段階では、状態管理ライブラリは何も入れないでOKな気がしています。
終わりに
色々書いてきましたが、みんな作っているプロダクトも、事業領域も、メンバーの保有スキルも、全部違います。
話をひっくり返すようですが、個々の置かれた状況の多様性を無視して、ベストな技術スタックは何かと十把一絡げにしている本記事の内容には相当無理があります。
そうではなくて、ウチのチームにぴったりFitする選択肢は何かという方向で、議論していかなくてはと思います。
そして、そうした議論の過程を記録に残しておくことも重要です。
ということで最後に@souppowerさんのArchitecture Decision Records(ADR)
に関する記事を貼らせてください🙇♂️
。。。
なんかbulletproof-react
にかこつけて、フロントエンドの雑多なトピックについて、徒然なるままに書いた形になってしまいました。
どちらかというと「2022年のReactの話題まとめ」みたいなタイトルが適切だったかもしれません。
twitterなどで気軽に感想を教えてもらえると嬉しいです。
あくまで素人の意見なので、どうかお手柔らかにお願いします🙏
最後までお読みいただきありがとうございました!!
Discussion
「なぜContextAPIでは駄目なのか?」というとdispatchだけしてstateには無関心なコンポーネントも再描画されるからではないかと思います。
なのでyoutubeの動画ではstateとdispatchでproviderを分けていると思います。
で、そういうことをしているとproviderが増えまくるなぁということで、何かライブラリ使おうという流れになる気がします。
なるほど〜
確かにproviderが増えまくるのは嫌ですし、両方必要なときに
みたいに2個useContextを呼び出さないといけないのもちょっとダルいですね。
となると、やはりjotaiやrecoilを入れた方が良さそうな気もして気もしきます。
(うーん、悩ましい..🤔)
コメントありがとうございました!
こういうことかなと。
ContextとProviderがいずれこうなるから…
拒否派と擁護派が一定数いるみたいですけれど。
これはつらい
波動拳打ってる方もいますね