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
規模に合わせて、段階的に?フォルダ構成を変えるのが良い?。以下も大規模になった際は、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」が勉強になりすぎる件
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> ); }
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:
- 【React/Vue.js】コンポーネント設計の(個人的)ベストプラクティス | Offers Tech Blog
- クリーンな React プロジェクトの 21 のベストプラクティス
- React のベストプラクティスとコード削減パターン - パート 1
- React のベストプラクティスとコード削減パターン - パート 2
- React のベストプラクティスとコード削減パターン - パート 3
CSS (Style)
命名は BEM で書くより rscss の方が良い(コンポーネントでクローズドにできるため BEM は冗長)
refs:
styled-components vs emotion
- 可読性に関して、emotion > styled-components は同意。
- 速度も emotion 優位らしい。
- emotion は特定のフレームワークにロックインされない。
- emotion なら Object Style で書ける。
- emotion の欠点は SSR 環境への導入はややこしいらしい。
refs:
- Vite+React+TypeScript で、CSS スタイルについて調べて、Emotion に流れ着いた
- CSS-in-JS のライブラリとして「emotion」を選択している理由
- emotion - フレームワークに依存しない洗練された CSS-in-JS
- Sass から CSS Modules、そして styled-components に乗り換えた話
- CSS in JS として Vanilla-Extract を選んだ話と技術選定の記録の残し方 (emotion vs Vanilla-Extract)
- パフォーマンスが重要なアプリケーションの場合は 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ファイルに書かれていると何が何かパッと見見づらい)
- 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
参考:
- Emotion Doc: Best Practices
- @emotion/react でコンポーネントの外部からスタイルを受け取る方法
- CSS in JS ライブラリ「emotion」のすすめ
- Emotion を使いこなす
- Emotion はいいぞ
- 【お遊び?】Emotion で Utility First してみた
- 以下のような指定もできるし、配列指定で合成も可能 ⇒ 一部 style を共通化して使用することも可能
<div css={[ css`color: white;`, // 1) テーマを引数に取る指定も含められる (theme) => css`background-color: ${theme.colors.primary};`, // 2) && etc. による条件付き指定も可 selected && css`background-color: red;`, ]} >
theme の モードチェンジ
refs:
- Implementing dark mode in next.js with emotion - Topcoder
- Adding Dark Mode to Your React App with Emotion CSS-in-JS
- [React + Typescript] emotion の Theming 機能を使って複数の Theme 切り替えを実装してみた
- emotion 公式 Doc:複数 theme サポートしないならば theme は使用すべきでない
※ 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: "お名前フォーム", }, };
- play 関数によりインタラクティブストーリーが記載可能
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
Router の種類(一部のみ。ドキュメント見るべし)
ref: ルーティングライブラリ、React Router(v5)入門
- BrowserRouter:HTML の History API(pushState、replaceState、popstate イベント)を使用して UI を URL と同期させるルーター
- HashRouter:URL のハッシュ部分(window.location.hash)を使用して UI を URL と同期させるルーター
- StaticRouter:location を変更しないルーター
- MemoryRouter
以下のように、コンポーネント化も可能(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 の分離
- storybook でのテストのカバレッジ向上目的
- 別の解決の手はあるらしい: 「3 種類」で管理する React の State 戦略: Local State
ref:
ビューをアプリケーションロジックから分離(関心の分離を促進)
- プレゼンテーションコンポーネント
- 役割:props を通じてデータを受け取り、受け取ったデータを変更することなく、スタイルも含めて意図通りに表示すること
- プレゼンテーションコンポーネントは(UI のためにステートが必要な場合を除き)ステートを持たない
- コンテナコンポーネント
- 自身が含むプレゼンテーションコンポーネントにデータを受け渡すこと
Pros:
- プレゼンテーションコンポーネントは
- UI を担当する純粋関数(=テストが容易)
- 再利用が容易(アプリケーション全体で異なる目的のために再利用可能=全体での一貫性を保てる)
- (デザイナーとの) 分業化容易 Cons:
- 小規模なアプリケーションでは過剰=複雑化
多くの場合、コンテナ・プレゼンテーションパターンは React のフックに置き換えることができる(コンテナ=カスタムフック)
テスト
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:テンプレートからファイル生成に使える
以下でも利用している。参考になるはず