サーバーサイドの人を React に引き込む作戦――コンポーネントベースの考え方をまとめる
Next.js 14 の keynote で、「server actions を使って、ボタンと同じファイル内に SQL が書ける」というキャッチーな光景が、賛否両論を呼んだ。
私個人としては、問題だとは思わない。 なぜなら、既存の「分断された クライアント / サーバー を、BFF も無しでがんばって繋げる」という世界が壊れて、
- A. コンポーネントに基づいたアプリケーション設計
- B. サーバー側のロジック(→必要に応じて、BFF と 真のサーバーに分かれる?)
- C. UI の細かな調整(→デザイナと連携 or 融合して消滅?)
という形の分業に変わり、フロントエンドエンジニア全員が B. まで担当する必要が無いと考えているからである。
この3つのグループは、ソースコードレベルで連携することができるので、従来のような openAPI およびソースジェネレータに頼った開発よりも連携が容易くなるはずだ。
そこで、
- A も B もできるエンジニア
- デザインを凝る必要のないアプリであれば、A から C まで一気通貫でできるエンジニア
が居ると生産性が上がるような気がするので、
ここでは サーバーサイド(あるいはモダンフロントエンドに親しんでいない Web 全般の)エンジニアが、Bだけでなく A、欲を出せば C まで出来るようになる ようになるのが最も安全で、手っ取り早いと思うので、
手引になりそうな React に関する情報を集める。ただし、TypeScript に関する解説は省略する。
公式の新ドキュメントで概要を掴む
何を差し置いてもこれ。 React のエッセンスが詰まっている。C. に寄った情報も含まれるが、「useEffect で状態を連携させる」みたいな、React に関する勘違いを晴らしてくれるような情報がしっかり詰まっている。
しかも、 React のパラダイムの理解困難さについても、HTML / CSS から入った人よりも、バックエンドが扱える(特に関数型プログラミングに馴染んだ)人にとっては、それほど難しくないはず。どちらかというと後者に近い思想で作られてそうなので。
アプリのコンポーネント・ディレクトリ構成についての考え方
アプリのコンポーネント・ディレクトリの構成についての考え方をざっとまとめた資料です。
- Container (サーバーとの通信やクエリパラメータの操作などを受け持つ) で全体の骨格を作る (A. 寄り)
- 細かなインタラクションを肉付けする(C. 寄り)
という構成にすると良いのではないか?という提案です。
Server / Client を自由に組み立てるためにはコンポジションが必要です。
ディレクトリ構成(Co-location 原則)についてはこちらも
状態管理を容易にするためのコンポーネント構成がそのまま使えます。
式・文がいつ実行されて、オブジェクトがいつ開放されるか整理する
個別のケースよりも、とにかく置く場所によって式・文がどのタイミングで実行されるかざっと列挙したのが以下の記事になります。
よくある疑問1. 状態管理はどうするの?
- ふつうにステートを使う
- 基本的にはこれで良い
- クエリパラメータや、Next.js のアドバンスドなルーティング機能を使う
- UX が改善できるかも
- Cookie, セッション状態等を使う
- UX が改善できるかも
- ライブラリ等を使う(ここはまだベストが定まらない)
- フェッチ結果のキャッシュ(Server Component, use() が使えるかも)
- フォームの状態 (server actions が絡む。useFormStatus, useFormState で代替できるかも?)
に分けられると思います。
基本的には「ふつうにステートを使う」で良いはず。そこを基本として、クエリパラメータ、Next.js の Routing 機能、Cookie 等をケースバイケースで使用して、UX を改善するのが基本戦略になりそう。
ふつうにステートを使う
コンテクストを使う前に
コンテクストはとても魅力的です! しかし、これはコンテクストは使いすぎにつながりやすいということでもあります。いくつかの props を数レベルの深さにわたって受け渡す必要があるというだけでは、その情報をコンテクストに入れるべきとはいえません。
ここで紹介するように、コンテクストを使う前に検討すべきいくつかの代替案があります。
- まずは props を渡す方法から始めましょう。 ちょっと凝ったコンポーネントであれば、多くの props を多くのコンポーネントを通して受け渡すことは珍しくありません。(中略)
- コンポーネントを抽出して、children を JSX として渡す方法を検討しましょう。 (中略) たとえば、
<Layout posts={posts} />
のような形で、データを直接使わないビジュアルコンポーネントに post のようなデータを渡しているのかもしれません。代わりに、Layout
はchildren
を props として受け取るようにし、<Layout><Posts posts={posts} /></Layout>
のようにレンダーしてみましょう。これにより、データを指定するコンポーネントとそれを必要とするコンポーネントの間のレイヤ数が減ります。これらのアプローチがどちらもうまくいかない場合は、コンテクストを検討してください。
https://ja.react.dev/learn/passing-data-deeply-with-context#before-you-use-context
関連する記事
よくある疑問2. useEffect は闇なんでしょう?
「乱用されがち」なのが問題であって、適切に使っていれば問題ありません
とりあえず、「これは乱用なのでやめたほうが良い」ポイントを、有名な記事 "You might not need an effect" (エフェクトは必要ないかもしれない) で確認しましょう。
下の返信で触れていない箇所はこの記事で確認できます。
余談 useEffect is not a mistake
useEffect は、(使うべきか否かを含めて)プログラマに熟考を強います。しかし、よく考えることで UI のバグを減らせるように精密に設計されています。 その設計の妙を知りもせずに「useEffect is a mistake」とか言っている皮肉屋どもには、下の記事を投げつけてあげましょう。
失敗なのは「useEffect をよく分からずに乱用したコード(あるいは、乱用されないような情報発信が遅れてしまった可能性はある)」であって、「useEffect の設計そのもの」ではありません。
Derived State (派生する状態) は生の式だけでOK
コンポーネント関数の直下に書いた式はレンダリングのたびに再実行されるので useEffect は不要です。
ついでに、 信頼できる唯一の情報源(Singel Source of Truth)としてのステート と、そこから派生する状態 に分けて、UI の状態変化が伝播する流れを整理しましょう。
「このステートは〇〇が変わったとき以外は変わらない」ことがハッキリしているのでコードを読むときに考えるべきことが少なるので楽です。
Derived State 以外でも、ステートを一つにしたり、prop を通じて親子を連携したり、イベントハンドラの中に書いたり、「useEffect を使わないほうがストレートに書けて楽になる」ことが多いです。
// DON'T
// この setFull はコンポーネントのどこからでも呼ばれる可能性がある
- const [fullName, setFullName] = (familyName + "" + personalName);
- useEffect(() => {
- setFullName(familyName + " " + personalName);
- }, []);
// DO
// fullName が変化するタイミングは familyName, personalName が変わるときだけ
+ const fullName = familyName + " " + personalName;
その "初期化"、useState で出来ます。
「useEffect の依存配列に空配列を入れると初期化処理」と考えられていることが多く見受けられていますが、これは大間違いです。
初期値が定数の場合
わざわざ useEffect で setCount を叩く必要はありません。二度手間です。
- const [count, setCount] = useState(0);
- useEffect(() => {
- setCount(10);
- }, []);
+ const [count, setCount] = useState(10)
初期値が定数でない場合
初期値が定数でない場合であっても、key を使ってコンポーネントそのものを初期化する方法があります。この方法は Server Component とも良さそうなので覚えておく価値があります。
key でコンポーネント自体を初期化するコード例
// hash 関数は、JSON.stringify などで string (あるいは number)型に変換します。
// ライブラリを使用したほうが良いかな...?
return (
<TaskEditForm
// key を書かないと、 データを取得しても title が空のままで更新されない
key={hash(data)}
defaultValues={data ?? { title: "" }}
/>
);
// TaskEditForm.tsx
export type TaskEditFormValues = {
title: string;
};
type Props = {
defaultValues: TaskEditFormValues;
};
const TaskEditForm: FC<Props> = ({ defaultValues }) => {
const [values, setValues] =
useState<TaskEditFormValues>(defaultValues);
クラスをインスタンス化する
「動的な処理を管理するためのクラスインスタンスを生成する」といった初期化処理は、ふつうに useState で大丈夫です。
const [queryClient] = React.useState(() => new QueryClient())
このように、useState
に渡した () => 初期値
の関数で新しいオブジェクトを生成しても、レンダリングの結果に影響を与えない限り問題ありません。
useEffect で扱ってるイベント登録は初期化じゃないよ
じゃあ、useEffect って何なの?初期化じゃないの?
「イベントを登録する」のに使うのが useEffect の本来の使い方ですが、これは「初期化」ではありません。(「エフェクト内で使われている値が変更したら、再登録してしかるべき」なことを誤魔化さずに表に出しているのが useEffect の大きな特徴です。)
▼ (疑問2. の上部ですでに載せていますが、再掲)
データ取得は 3rd party ライブラリ・Server Component・Suspense とか
App Router 以前では、データ取得ライブラリ(SWR とか TanStack Query とか)を使いましょう。自分で useEffect を書くのは骨が折れます。
新たな選択肢としては、
- Server Component (普通に async が使える)
- 煩雑だった SSR はこちらに移行してシンプルになりそう
- クライアント側なら Suspense,
use()
とかを使う- SWR と TanStack Query もここで使える
Suepsne と 3rd-party ライブラリの連携や use()
の利用はまだ安定しない?ようですが、そのうち安定して、実装パターンが固まってくる。 といった感じです。
よくある疑問3. スタイリング方法が多すぎてキャッチアップできない
とりあえず CSS Module + SCSS でだいたい書けます
ただし、Tailwind CSS も勃興しそう(生成 UI と相性が良さそうなので)
- 動的なスタイル
-
data-
属性 -
style
Prop (inline style) - CSS 変数
-
- 長い記述も短縮できる
- CSS Module だとクラス名は短めで済ませられます
-
data-
のお陰で Modifier なクラスが不要になる -
gap
とか CSS Grid とか Flexbox を活用しまくる- iOS の旧バージョンさえ気をつければ、後方互換性も気になりませんし
- SCSS を使う
- 定数に基づいて計算したり、function を使ったりできる(マジックナンバーよりも意図が分かりやすい)
- mixin とかでメディアクエリを共通化できる
- CSS Modules も SCSS もデファクト
- Next.js でも Vite でも
npm i scss -D
するだけで基本の設定が終了する - CSS Modules は設定不要
- Next.js でも Vite でも
▼動的なスタイルを書く方法
▼CSS Grid を使えば、「子」ど「子をうまく並べる親」の分離がしやすい
Dialog とかが含まれるコンポーネント集ライブラリはどうする?
-
(スタイル付き)コンポーネントライブラリ
- ルック&フィール(見た目)付きのライブラリー集
- テキスト入力、ドロップダウン、ダイアログ みたいな実装の面倒な UI の実装を省力化できる
- たぶん向いているプロジェクト
- デザイナー不在の簡易的に体裁を整える
- デザイナーと申し合わせて、デザインを統一してもらえる場合
- 例1: MUI Material (旧: Material UI)
- 例2: Chakra UI
- 例3: Mantine UI
- 例4: Radix UI Themes → 下の Radix UI Primitives の部分を参照
- ルック&フィール(見た目)付きのライブラリー集
- ✨ヘッドレスUIライブラリ とりあえずオススメ!
- ルック&フィールを持たず、className や style を自由に設定できる
- スタイル付きコンポーネントライブラリに対して、スタイルは自力で書く必要がある
- 裏を返せば、CSS でも CSS in JS (emotion, styled-component, Kuma UI, Panda CSS, etc.)でも何でも使える → ここが強い
- 例1: Radix UI Primitives
- 派生したライブラリ等のエコシステムも豊富な気がする
- Radix UI Themes ルック&フィールつきのライブラリ
- shadcn/ui Tailwind CSS 使用、コピペして取り込むコンポーネント集
- 派生したライブラリ等のエコシステムも豊富な気がする
- 例2: Headless UI
- 例3: Ark UI Chakra UI が開発したヘッドレス版ライブラリ
- 例4: MUI Base UI Material UI で有名な MUI のヘッドレス版ライブラリ (beta)
- ルック&フィールを持たず、className や style を自由に設定できる