Open12

サーバーサイドの人を React に引き込む作戦――コンポーネントベースの考え方をまとめる

Honey32Honey32

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 に関する解説は省略する。

Honey32Honey32

公式の新ドキュメントで概要を掴む

何を差し置いてもこれ。 React のエッセンスが詰まっている。C. に寄った情報も含まれるが、「useEffect で状態を連携させる」みたいな、React に関する勘違いを晴らしてくれるような情報がしっかり詰まっている。

https://ja.react.dev/learn

しかも、 React のパラダイムの理解困難さについても、HTML / CSS から入った人よりも、バックエンドが扱える(特に関数型プログラミングに馴染んだ)人にとっては、それほど難しくないはず。どちらかというと後者に近い思想で作られてそうなので。

Honey32Honey32

アプリのコンポーネント・ディレクトリ構成についての考え方

アプリのコンポーネント・ディレクトリの構成についての考え方をざっとまとめた資料です。

  • Container (サーバーとの通信やクエリパラメータの操作などを受け持つ) で全体の骨格を作る (A. 寄り)
  • 細かなインタラクションを肉付けする(C. 寄り)

という構成にすると良いのではないか?という提案です。

https://speakerdeck.com/honey32/mazu-container-yorishi-meyo?slide=24

Server / Client を自由に組み立てるためにはコンポジションが必要です。

https://qiita.com/honey32/items/bc24d8c0ea3d096ff956


ディレクトリ構成(Co-location 原則)についてはこちらも

https://qiita.com/honey32/items/dbf3c5a5a71636374567

状態管理を容易にするためのコンポーネント構成がそのまま使えます。

https://zenn.dev/link/comments/651cf249a6f557

Honey32Honey32

よくある疑問1. 状態管理はどうするの?

  • ふつうにステートを使う
    • 基本的にはこれで良い
  • クエリパラメータや、Next.js のアドバンスドなルーティング機能を使う
    • UX が改善できるかも
  • Cookie, セッション状態等を使う
    • UX が改善できるかも
  • ライブラリ等を使う(ここはまだベストが定まらない)
    • フェッチ結果のキャッシュ(Server Component, use() が使えるかも)
    • フォームの状態 (server actions が絡む。useFormStatus, useFormState で代替できるかも?)

に分けられると思います。

基本的には「ふつうにステートを使う」で良いはず。そこを基本として、クエリパラメータ、Next.js の Routing 機能、Cookie 等をケースバイケースで使用して、UX を改善するのが基本戦略になりそう。

https://zenn.dev/knowledgework/articles/607ec0c9b0408d

https://zenn.dev/yumemi_inc/articles/react-lifetime-of-variable

Honey32Honey32

ふつうにステートを使う

コンテクストを使う前に

コンテクストはとても魅力的です! しかし、これはコンテクストは使いすぎにつながりやすいということでもあります。いくつかの props を数レベルの深さにわたって受け渡す必要があるというだけでは、その情報をコンテクストに入れるべきとはいえません。

ここで紹介するように、コンテクストを使う前に検討すべきいくつかの代替案があります。

  1. まずは props を渡す方法から始めましょう。 ちょっと凝ったコンポーネントであれば、多くの props を多くのコンポーネントを通して受け渡すことは珍しくありません。(中略)
  2. コンポーネントを抽出して、children を JSX として渡す方法を検討しましょう。 (中略) たとえば、<Layout posts={posts} /> のような形で、データを直接使わないビジュアルコンポーネントに post のようなデータを渡しているのかもしれません。代わりに、Layoutchildren を props として受け取るようにし、<Layout><Posts posts={posts} /></Layout> のようにレンダーしてみましょう。これにより、データを指定するコンポーネントとそれを必要とするコンポーネントの間のレイヤ数が減ります。

これらのアプローチがどちらもうまくいかない場合は、コンテクストを検討してください。

https://ja.react.dev/learn/passing-data-deeply-with-context#before-you-use-context

関連する記事

https://qiita.com/honey32/items/b9f70f960e891f031b0f

https://qiita.com/honey32/items/4d04e454550fb1ed922c

Honey32Honey32

よくある疑問2. useEffect は闇なんでしょう?

「乱用されがち」なのが問題であって、適切に使っていれば問題ありません

とりあえず、「これは乱用なのでやめたほうが良い」ポイントを、有名な記事 "You might not need an effect" (エフェクトは必要ないかもしれない) で確認しましょう。

https://ja.react.dev/learn/you-might-not-need-an-effect

下の返信で触れていない箇所はこの記事で確認できます。

余談 useEffect is not a mistake

useEffect は、(使うべきか否かを含めて)プログラマに熟考を強います。しかし、よく考えることで UI のバグを減らせるように精密に設計されています。 その設計の妙を知りもせずに「useEffect is a mistake」とか言っている皮肉屋どもには、下の記事を投げつけてあげましょう。

https://zenn.dev/yumemi_inc/articles/react-effect-simply-explained

失敗なのは「useEffect をよく分からずに乱用したコード(あるいは、乱用されないような情報発信が遅れてしまった可能性はある)」であって、「useEffect の設計そのもの」ではありません。

Honey32Honey32

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;

https://qiita.com/honey32/items/58e56e407d4d87e294a4

https://youtube.com/shorts/hY9jxevPO9c?si=7hWzfdDXIfNPFy0A

Honey32Honey32

その "初期化"、useState で出来ます。

「useEffect の依存配列に空配列を入れると初期化処理」と考えられていることが多く見受けられていますが、これは大間違いです。

初期値が定数の場合

わざわざ useEffect で setCount を叩く必要はありません。二度手間です。

-  const [count, setCount] = useState(0);
-  useEffect(() => {
-    setCount(10);
-  }, []);

+ const [count, setCount] = useState(10)

初期値が定数でない場合

初期値が定数でない場合であっても、key を使ってコンポーネントそのものを初期化する方法があります。この方法は Server Component とも良さそうなので覚えておく価値があります。

https://zenn.dev/yumemi_inc/articles/react-initial-state-take-advantage

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())

https://tanstack.com/query/v4/docs/react/guides/ssr#using-hydration

このように、useState に渡した () => 初期値 の関数で新しいオブジェクトを生成しても、レンダリングの結果に影響を与えない限り問題ありません。

useEffect で扱ってるイベント登録は初期化じゃないよ

じゃあ、useEffect って何なの?初期化じゃないの?

「イベントを登録する」のに使うのが useEffect の本来の使い方ですが、これは「初期化」ではありません。(「エフェクト内で使われている値が変更したら、再登録してしかるべき」なことを誤魔化さずに表に出しているのが useEffect の大きな特徴です。)

▼ (疑問2. の上部ですでに載せていますが、再掲)

https://zenn.dev/yumemi_inc/articles/react-effect-simply-explained

Honey32Honey32

データ取得は 3rd party ライブラリ・Server Component・Suspense とか

App Router 以前では、データ取得ライブラリ(SWR とか TanStack Query とか)を使いましょう。自分で useEffect を書くのは骨が折れます。

新たな選択肢としては、

  • Server Component (普通に async が使える)
    • 煩雑だった SSR はこちらに移行してシンプルになりそう
  • クライアント側なら Suspense, use() とかを使う
    • SWR と TanStack Query もここで使える

Suepsne と 3rd-party ライブラリの連携や use() の利用はまだ安定しない?ようですが、そのうち安定して、実装パターンが固まってくる。 といった感じです。

Honey32Honey32

よくある疑問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 は設定不要

▼動的なスタイルを書く方法

https://zenn.dev/okamoai/articles/ae39b3838d1b75

▼CSS Grid を使えば、「子」ど「子をうまく並べる親」の分離がしやすい

https://qiita.com/honey32/items/590c756bbcd5de75f607

Honey32Honey32

Dialog とかが含まれるコンポーネント集ライブラリはどうする?

  1. (スタイル付き)コンポーネントライブラリ
    • ルック&フィール(見た目)付きのライブラリー集
      • テキスト入力、ドロップダウン、ダイアログ みたいな実装の面倒な UI の実装を省力化できる
    • たぶん向いているプロジェクト
      • デザイナー不在の簡易的に体裁を整える
      • デザイナーと申し合わせて、デザインを統一してもらえる場合
    • 例1: MUI Material (旧: Material UI)
    • 例2: Chakra UI
    • 例3: Mantine UI
    • 例4: Radix UI Themes → 下の Radix UI Primitives の部分を参照
  2. ヘッドレス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)