🤹‍♂️

(Next13 /app) React Server Components 時代の状態管理術【前編】

2023/04/17に公開

前編と後編の2部構成にしています。

  • 前編は【今までのReactらしい状態管理術】
  • 後編は【Next.js 13 /app (React Server Components) らしい状態管理術】

の説明です。両者を比較します。

コンポーネント単位でサーバー側のレンダリングを選択できるので、GraphQLでなくてもバックエンドへのアクセスを纏められたり、あらかじめサーバー側でデータを加工することで、バンドルサイズ0を目指せる React Server Components
これは周知の通り、クライアントコンポーネントとサーバーコンポーネントに分かれますが、

useState Recoil などはサーバーコンポーネントでは利用できないので、

Next.js 13 /app (React Server Components) らしい状態管理術はこんな感じになります。

https://github.com/vteacher-online/react-tree-demo/blob/master/app/rsc/page.tsx

サーバーコンポーネントは `async` が使える!

サーバーコンポーネントは非同期にできます。
async が使えて Promise<Suspense> で吸収できます。

  • サーバーコンポーネントB
export default async function B() {
  const res = await fetch('https://...', {
    method: 'GET',
  });
  ...
}
  • クライアントコンポーネントB(呼び出し元)
<B>
  <Suspense fallback={<div>Loading...</div>}>
    {/* @ts-expect-error Async Server Component */}
    <B_server a={a} d={d} />
  </Suspense>
</B>

※TypeScriptがこの書き方に未対応のため、 {/* @ts-expect-error Async Server Component */} をつけて消しましょう( 公式 より )。

※ちなみにサーバーとクライアントは コンポジション 構成にしたほうがやりやすいです。

Nextが同じ内容のfetchを自動重複排除!

複数のコンポーネントで同じデータを取得する必要がある場合、Next.js は一時キャッシュに同じリクエストを持つfetchを自動的にキャッシュしてくれます。この最適化により、レンダリング中に同じ内容のfetchが複数回呼び出されるのを防いでくれますので、より コロケーション の概念でやれます。
https://beta.nextjs.org/docs/data-fetching/fundamentals

(Metaのデモを参考に、URIに状態を持たせています。他にCookieを使う方法もあります)

nrstate を使ってRSCの状態管理をする場合はこちら)

`nrstate` を使ってRSCの状態管理をする

nrstate

State for React Server Components (RSC) on Next.js

N: Next.js
R: React Server Components
State

Features

✨ Next.js 13 support, for RSC

  • Read/Write state also works on Client-component.
  • Read state also works on Server-component.

Quick start

npm i nrstate
npm i nrstate-client

You can play with the example code here.

https://github.com/vteacher-online/nrstate-demo

Server and Client Components Observation

Implement persistence, read or write by observing all state changes across your page, without impairing code-splitting.

PageState

A PageState represents a state of Page-component.

  • Client-component
    PageState can be read and write from any Client-component.

  • Server-component
    PageState can be read from any Server-component.

ex.
Demo

  • PageStateDemo.tsx
export const pathDemo = '/demo';

export type PageStateDemo = {
  a: string;
  d: string;
};

export const initialPageStateDemo = { a: '', d: 'asc' } as PageStateDemo;

PageStateProvider

Components that use nrstate need <PageStateProvider> to appear somewhere in the parent tree. A good place to put this is in your root component.

Make the "page" Server-component.
( Do not write "use client" )

  • page.tsx
export default function Page() {
  return (
    <PageStateProvider
      current={currentPageState<PageStateDemo>(
        initialPageStateDemo,
        pathDemo,
      )}
    >
      <></>
    </PageStateProvider>
  );
}

Read PageState

  • Client component
'use client';
export default function ClientComponent() {
  const [pageState, setPageState] = usePageState<PageStateDemo>();
  const { a, d } = pageState;

  return (
    <>
      a={a}, d={d}
    </>
  );
}
  • Server component
export default async function ServerComponent() {
  const pageState = getPageState<PageStateDemo>(initialPageStateDemo, pathDemo);
  const { a, d } = pageState;

  return (
    <>
      a={a}, d={d}
    </>
  );
}

Write PageState

Client-component can use this.
PageState updates will result in a re-render of all components subscribed to that Page-component.

'use client';
export default function ClientComponentInput() {
  const [pageState, setPageState] = usePageState<PageStateDemo>();
  const { a } = pageState;
  return (
    <input type="text" onChange={(e) => {
      setPageState({
          ...pageState,
          a: e.target.value,
        }, pathDemo);
      }}
    />
  );
}

useState and usePageState can be used together.

You can play with the example code here.

https://github.com/vteacher-online/nrstate-demo

Then, http://localhost:3000/

では、まずは前編を🙇‍♀️

VTeacher所属のSatokoです。

QAエンジニアとフロントエンドエンジニアを兼任しています。

最近、Recoilおじさんという言葉を聞きました。
大昔にstaticおじさんという言葉が流行ったらしいのですが、それのReact版だそうです。

はい、ご察しの通り、私は社長に Recoilおじさん と呼ばれました・・・。

(せめて、Recoilおねえさん・・・)

さて、本投稿は、
React Server Components 時代の状態管理術の前編ということで、今までの「Reactらしい状態管理術」について、勉強会にて私が怒られたところを中心に書いていきたいと思います。

コンポーネントツリーを描いていない問題

そもそもコンポーネントツリーを描いていないという問題がありました。
(Storybookばかりいじっています)

コンポーネントツリーは次のようにコンポーネントの親子関係をツリー構造で表した図のことです。Reactの勉強をしていると、必ずと言って良いほどこの図に遭遇すると思います。

ちなみに、このコンポーネントツリーをJSXで書くと、次のようになります。
※🕍はRoot=根となるコンポーネントなので省略しています。

<>
  <👩>
    <👦 />
    <👧 />
  </👩>
  <🐔>
    <🐤 />
  </🐔>
</>
Tree structures (ツリー構造)について

Tree structures (ツリー構造)

データ構造のひとつで、グラフ理論の木の構造をしています。
頂点・節点をノード、ノード間を結ぶ枝・辺をエッジと呼ぶことがあります。
https://react.dev/learn/preserving-and-resetting-state#the-ui-tree

ツリー構造はReactをやっていくうえでとても重要です。

React は木構造を使用して、作成した UI を管理およびモデル化します。 React は JSX から UI ツリーを作成します。次に、React DOM はブラウザの DOM 要素を更新して、その UI ツリーに一致させます。

https://www.youtube.com/watch?v=pLMyh9wosBk&t=7671s

コンポーネントツリーは何のために作るの?

UI = fn(state)

この式は 「表示内容(UI)が状態(state)に依存する関数(fn)であること」 を意味する数学的表現で、Reactではこの概念を「状態に基づくレンダリング」と呼びます。
これは、常に現在の状態に基づいた正確な表示内容を維持することができるという意味です。

注意点ですが、
無計画に作ってしまうと、その状態と無関係なUIも再レンダリングの対象になってしまうという、パフォーマンス的な問題(無駄な再レンダリング)が発生しがちです。

この無駄な再レンダリング問題を解消するときに、コンポーネントツリーが役立ちます。
(memo化やキャッシュとは別軸の、根本的なパフォーマンス改善のお話です)

React Developer Tools

無駄な再レンダリングを調査する際、Metaから React Developer Tools が出ているので、それを使用しましょう。
Chromeの拡張アドオン: React Developer Tools

React Developer Tools をインストール後、Chrome を開き、画面右クリック→検証から、Chrome DevTools。Components タブを選択し、Highlight updates when components render にチェック。

これでレンダリングされた場所を強調表示できます(緑色の枠)。

混同しがちな2つの考え方(同じ「小さく分ける」でも)

コンポーネントツリーの作成にあたり、まずは複数のコンポーネントに分割する必要があります。
次の2つは、両方とも「小さく分ける」やり方ですが、アプローチが異なります。

  1. 「コンポーネント優先」 で考えると、先に複数のコンポーネントに小さく分けてから、それらを組み合わせてページ全体を作っていく(ボトムアップ)。
  2. 「UI = fn(state) 優先」 で考えると、先にページ全体を作ってから、各状態の依存範囲ごとにUIを小さく分けていく(トップダウン)。

そして、私たちが参加している勉強会では、

前者(1)をコンポーネントファーストな設計と呼んでいます。
後者(2)をファンクション(関数)ファーストな設計と呼んでいます。

本投稿では、同じ例題を使って、両者を比較してみます!

例題

  • 検索フォーム入力
  • 検索キーワードとソート順から検索結果一覧表示
  • メニュー一覧表示
  • ソート順選択
  • 検索キーワードから関連広告表示

状態(state)はどれ?
図の矢印は(入力)→(出力)の関係を表しています。
「時間により変化する値」を状態(state)と捉え、この例題の場合、ユーザーがいつでも自由に変更できる「検索フォームの入力値」と「ソート順の入力値」を状態(state)とします。

コンポーネントファースト

コンポーネントファーストによる設計とは?
あらかじめ用意した複数のコンポーネントを、デザイン案を元に組み立てて、ひとつのページを作り上げていくやりかたです。

従来のフロントエンドの作りかたにとても近いので、つい(というか無意識に)、コンポーネントファーストで作っていると思います。

※Atomic Design や BEM を元に作業することも多いかと思います。

参考:Atomic Design
参考:BEM

では、次に、コンポーネントファーストの具体的な例を見ていきましょう。

Step 1. コンポーネントを分割

コンポーネントファーストでは、最初にUIを部品のように捉えて、それをコンポーネントとして分割してしまいます。

先ほどの例題で言うと、次のようになります。

  • 検索フォーム → コンポーネントA
  • 検索キーワードとソート順から検索結果一覧 → コンポーネントB
  • メニュー一覧 → コンポーネントC
  • ソート順 → コンポーネントD
  • 検索キーワードから関連広告 → コンポーネントE

Step 2. コンポーネントを組み合わせる

分割したコンポーネントを、デザイン案に沿って組み合わせます。そして、HTMLを書くような感覚でJSXを書いてしまいます。

この例ですと、JSXは次の通りです。

※デザイン案に沿って、左・右をdivブロック( Tailwind CSS を使用)にまとめています。

<>
  <div className="float-left w-1/3">
    <C />
    <E />
  </div>
  <div className="float-right w-2/3">
    <A />
    <D />
    <B />
  </div>
</>
Tailwind CSS について

記事中に出てくるCSS(Tailwind CSS)の説明:

  • float-left :左側に沿うように配置する
  • float-right :右側に沿うように配置する
  • w-1/3 :画面横幅の1/3の幅にする
  • w-2/3 :画面横幅の2/3の幅にする

参考:
https://zenn.dev/rgbkids/articles/7bd919464283d0

Step 3. コンポーネントツリーを作成

Step 2 で作成したJSXからコンポーネントツリーを起こしてみます。

この例ですと、次のようになります。

注意点
コンポーネントファーストで作ったコンポーネントツリーは、各コンポーネント間の状態依存を表せていません。
例えば、「コンポーネントAが持つ状態(検索フォームの入力値)を、どのようにコンポーネントB・Eに渡せば良いのか」というところが、図からは読み取れません。

どのようにすれば良いのか?
それはたいてい、グローバルステート(例えばRecoil)を使って解決します。

コンポーネントファーストの場合、良くも悪くもグローバルステート

グローバルステートを使用すると、本来は親子関係がないコンポーネント間であっても、疑似的に親子関係を結ぶことができます。その場合、親から子に値を渡すにはprops経由ではなく、グローバルステート経由になります(propsと併用はできます)。

そもそもpropsとは何なの?

Reactの公式ドキュメントを読むと、次のように書かれています。

  • props
    そのコンポーネントの呼び出し元によって定義された値。
  • state
    そのコンポーネント固有の値(通常、stateは時間の経過とともに変化する)。

https://react.dev/learn/passing-props-to-a-component

ReactDOM conforms to the HTML standard
(ReactDOM はHTML 標準に準拠しています)

• To pass props, add them to the JSX, just like you would with HTML attributes.
(props を渡すには、HTML 属性の場合と同様に、それらを JSX に追加します)

https://www.componentdriven.org/

propsはHTMLで言うところのattributeそのものに見えますが、もともとは(出始めのreact-0.1Xのころなどは)、propsはもっと明確に「親の値」でした。実際、propsで渡される値には、その親コンポーネントが持つ状態のコピーやそれに紐づく情報が含まれることが多いでしょう。

グローバルステートですが、基本的に、どのコンポーネントからも値の変更&取得が可能なので、そこが良いところであり、悪いところでもあります。とても簡単に説明してしまうと、グローバルステート≒グローバル変数です。注意しながら使っていく必要があります。

コンポーネントツリーに疑似的な親子関係を表現する

グローバルステートによる状態依存を表すため、先ほど作成したコンポーネントツリーに、点線の矢印を加えます。

これにより 擬似的な親子関係 が表現されます。

  • Bを表示するには、Aの検索キーワードとDのソート順を必要とするので、
    • A - - → B
    • D - - → B
  • Eを表示するには、Aの検索キーワードを必要とするので、
    • A - - → E

Recoilで実装する場合、次のようになります。

(※ここでは A - - → E の実装例のみ記載。全コードは文末にあります)

コンポーネントAのグローバルステートを_Aとします。

  1. コンポーネントAの状態が変更
    • グローバルステート_Aを変更
      • コンポーネントEが、グローバルステート_Aの変更を検知
  2. コンポーネントAだけでなく、コンポーネントEも再レンダリングされる
/* グローバルステート(_A)を定義する。 */

const _A = atom({
  key: 'GlobalState_A',
  default: '',
});
/* コンポーネントAで検索キーワードが入力されたら、グローバルステート(_A)を変更する。 */

export default function A() {
  const [_a, set_A] = useRecoilState(_A);

  return (
    <div>
      <p>A</p>
      <input
        type="text"
        defaultValue={_a}
        onChange={(e) => {
          set_A(e.target.value);
        }}
      />
    </div>
  );
}
/* Aの状態が変更されると、Eは再レンダリングされ、          */
/* グローバルステート(_A)から変更後の状態の値を取得できる。 */

export default function E() {
  const [_a] = useRecoilState(_A);

  return (
    <div>
      <p>E</p>
      <p>AdWords {_a}</p>
    </div>
  );
}

React Developer Tools

コンポーネントファーストの実装例を、React Developer Tools(Componentsタブ)を使って確認してみます。緑色の枠の中が、状態変更時の再レンダリングの場所です。

コンポーネントファーストのメリット・デメリット

  • メリット
    今までのCSS設計手法に近いので、デザイン案から自然な流れでコードに落とし込め、デザイナーも開発者もわかりやすい。
  • デメリット
    グローバルステートが前提になることが多く、状態管理の難易度が上がる傾向あり。

次に説明するファンクションファーストは、グローバルステートを多用せず、React本来の状態管理術の良さ(useStateなど)を最大限に活かした方法になります。

ファンクション(関数)ファースト

※こちらの記事も参考にしてみてください!
https://zenn.dev/rgbkids/articles/d7691b6c852b42

まず、頻出しているこの式をもういちど確認してみます。
これは、一次関数のように、状態(state)が決まれば、表示内容(UI)が決まるという意味でした。

UI = fn(state)

では、この式を踏まえて、ファンクションファーストな設計でコンポーネントツリーを作っていきます

※コンポーネントファーストのときと同じ例題を使って、両者を比較してみます。

  • 検索フォーム入力
  • 検索キーワードとソート順から検索結果一覧表示
  • メニュー一覧表示
  • ソート順選択
  • 検索キーワードから関連広告表示

Step 1. 状態の依存関係を調べる

コンポーネントファーストのときと同様、状態を次のように捉えます。

  • 検索フォームの入力値
  • ソート順の入力値

状態(state)と表示(UI)の依存関係を整理してみましょう。

例題では次のようになります。

表示:検索結果 表示:関連広告
状態:検索フォームの入力値の変化 依存 依存
状態:ソート順の入力値の変化 依存

Step 2. レンダリングの範囲を分割する

Step 1 の結果から、より例題に合わせて UI=fn(state) を考えると、次のようになります。

UI = fn(検索フォームの入力値, ソート順の入力値)

これは「『検索フォームの入力値』または『ソート順の入力値』に変更があれば、画面の表示内容を再構築する」ことを意味しています。

重要な点
関連広告を表示するには、検索フォームの入力値を必要としますが、ソート順は使いません。しかし、素のままですと、ソート順だけ変更した場合でも、ソート順に無関係な関連広告も再レンダリングされてしまいます。前述の「コンポーネントツリーは何のために作るの?」でも触れましたが、これは UI=fn(state) という概念の欠点にあたるところで、パフォーマンス的に良くなく、動きがもっさりする原因になります。

なので、工夫が必要になります。

方針として、レンダリングの範囲を小さく分けていきます!
ひとつのページを、複数の UI=fn(state) に分けるということです

レンダリングの範囲を小さく分ける

では、どのように小さく分けていけば良いのでしょうか?

まずは、HTMLタグに着目し、それをベースにレンダリングの範囲を分けてみます。

  • 範囲A
<input type="text" />
  • 範囲B
<ul>
  <li>01: Tetsuo Yamada (infielder)</li>
  (...略...)
  <li>55: Munetaka Murakami (infielder)</li>
</ul>
  • 範囲C
<ul>
  <li>Menu 1</li>
  <li>Menu 2</li>
  <li>Menu 3</li>
</ul>
  • 範囲D
<select>
  <option>asc</option>
  <option>desc</option>
</select>
  • 範囲E
<p>AdWords infielder</p>

※これはたいてい、コンポーネントとなる分割単位と一致します。とくに今回は比較しやすいように、コンポーネントファーストの分割単位と同じにしています。

各範囲を UI = fn(state) で表す

では、小さく分けた各レンダリングの範囲(=小さく分けたUI)を、状態(state)を用いて、 UI=fn(state) で表してみます。

UIを大文字、状態を小文字、UIを作り出す関数をfnで表すなら、次のように表すことができます。

A = fnA()     // 範囲AのUIを返す関数
B = fnB(a, d) // 範囲BのUIを返す関数は、範囲Aにある状態(検索キーワード) と範囲Dにある状態(ソート順)が必要
C = fnC()     // 範囲CのUIを返す関数
D = fnD()     // 範囲DのUIを返す関数
E = fnE(a)    // 範囲EのUIを返す関数は、範囲Aにある存在する状態(検索キーワード)が必要

これだとちょっと見にくいので、カリー化というテクニックを使ってみます。

  • カリー化の例
// この関数は、
L = fnL(m, n)

// カリー化というテクニックを使うと、ラムダ表記(アロー関数を用いる書き方)で書き換えられる
fnL = (m) => (n) => L
カリー化というテクニックを復習(簡易版)
/* ------------------------------------------------------------
1. 次の関数があったとします。
------------------------------------------------------------ */

L = fnL(m, n)

// 具体的な例:
console.log(add(1,2)) // [Console] 3

/* ------------------------------------------------------------
2. 引数をひとつずつの関数を分けると、次のようになります(カリー化)。
------------------------------------------------------------ */

fnL(m) {
 return fn(n) {
  return L
 }
}

// 具体的な例:

function add(m, n) {
 return m + n
}
console.log(add(1,2)) // [Console] 3

// ↓これをこのようにする(JavaScriptは関数が第一級オブジェクトとなのでこう書ける)

function add(m) {
 return function(n) {
  return m + n
 }
}
console.log(add(1)(2)) // [Console] 3

/* ------------------------------------------------------------
3. これをラムダ表記(アロー関数を用いて書くと)にすると、次のようになります。
------------------------------------------------------------ */

fnL = (m) => (n) => L

// 具体的な例:

const add = (m) => (n) => m + n

console.log(add(1)(2)) // [Console] 3

これを踏まえると、さきほどの各関数は次のように書き換えることができます。
※引数が2つ以上ある場合は、とりあえず順不同でやってしまいます。

fnA = () => A         // A = fnA()
fnB = (a) => (d) => B // B = fnB(a, d)
fnC = () => C         // C = fnC()
fnD = () => D         // D = fnD()
fnE = (a) => E        // E = fnE(a)

Step 3. コンポーネントツリーを作成

Step 2 で作った式から、親子関係を表すことができます。
※🕍はRoot=根となるコンポーネントです。
※左からA、B、C、D、Eです。

各コンポーネントの依存関係を崩さないように重ねて(統合して)、ツリー構造で表してみます。

よく見ると、図の中にDが2つありました。

次の解決方法をとってみます。

  • M→O なら M→(N)→O も成り立つ(親からpropsを引き継げるので)

  • M→O なら
function M() {
  const [m, setM] = useState('');

  return <O m={m} />;
}

function O({ m }) {
  return <>m={m}</>;
}
  • M→(N)→O も成り立つ(親からpropsを引き継げるので)
function M() {
  const [m, setM] = useState('');

  return <N m={m} />;
}

function N({ m }) {
  return <O m={m} />;
}

function O({ m }) {
  return <>m={m}</>;
}

これに従って、Dの依存関係が崩れないように、2つあるDをひとつにします。

これでファンクションファーストによるコンポーネントツリーができました。

この図をJSXにしてみます。

無駄な再レンダリングの対策は、自然と施されていることになります。

※全コードは文末にあります。

<>
  <div className="float-left w-1/3">
    <C />
  </div>
  <div className="float-right w-2/3">
    <A />
  </div>
</>
  • A
<>
  <input type="text" />
  <D a={a} />
  <E a={a} />
</>
  • B
<>
  <ul>
    <li>01: Tetsuo Yamada (infielder)</li>
    (...略...)
    <li>55: Munetaka Murakami (infielder)</li>
  </ul>
</>
  • C
<>
  <ul>
    <li>Menu 1</li>
    <li>Menu 2</li>
    <li>Menu 3</li>
  </ul>
</>
  • D
<>
  <select>
    <option>asc</option>
    <option>desc</option>
  </select>
  <B a={a} d={d} />
</>
  • E
<>
  <p>E</p>
  <p>AdWords {a}</p>
</>

ツリー構造とデザイン案が一致しない場合

実はこのツリー構造ですと、デザイン案に厳密に沿う場合、うまく書けないところがあります。それは「デザインとして左右にブロックが分かれており、EをAの子孫として配置することが難しい」という点です。

  • Eはaを必要とするのでAの子だが、デザイン的にleftのほうに置きたい
<>
  <div className="float-left w-1/3">
    <C />
  </div>
  <div className="float-right w-2/3">
    <A /> {/* ←Eはaを必要とするのでAの子だが、デザイン的にleftのほうに置きたい */}
  </div>
</>

ひとつ階層を上げてみる

仕方がないので、Eだけレイヤーを1階層あげてみます。

AとEの状態依存(親子関係)を擬似的に保つため、AとEの間だけ、グローバルステートを使います。
※点線矢印の部分です。

(ピンポイントでグローバルステートを許可することで、Recoilおじさん化を防ぎます)

コンポーネントファーストのときのように、デザイン案に沿ってleftとrightのdivブロックで囲むなら、次のようになります。

(上の図と状態依存は変わっていません)

このコンポーネントツリーをJSXで書いてみます。

次のようになります。

()

<div className="float-left w-1/3">
  <C />
  <E /> {/* ←グローバルステートを使うことで、依存関係を崩さずに、left側にEを持ってこれる */}
</div>
<div className="float-right w-2/3">
  <A />
</div>

()

※コンポーネントAとコンポーネントEはグローバルステートで結びます。

詳細
export default function A() {
const [ a, setA ] = useState('');
const [_a, set_A] = useRecoilState(_A);

return (
<>
  <input type="text" defaultValue={a}
    onChange={(e) => {
      setA(e.target.value);
      set_A(e.target.value); // ←Aでグローバルステート_Aを変更
    }}
  />
  <D a={a} />
</>
);
}
export default function E() {
const [_a] = useRecoilState(_A);

return (
<div>
  <p>E</p>
  <p>AdWords {_a}</p> {/* ←Eでグローバルステート_Aを表示 */}
</div>
);
}

※全コードは文末にあります。

ちなみにコンポーネントファーストで作ったときのJSXはこれでした(比較用)。

()

<div className="float-left w-1/3">
  <C />
  <E />
</div>
<div className="float-right w-2/3">
  <A />
  <D />
  <B />
</div>

()

コンポーネントファーストで作ったときのコンポーネントツリーはこれでした(比較用)。

※ファンクションファーストのほうが、上から下(根から葉)の流れが自然だと思います。

React Developer Tools

ファンクションファーストの実装例を、React Developer Tools(Componentsタブ)を使って確認してみます。緑色の枠の中が、状態変更時の再レンダリングの場所です。

ファンクションファーストのメリット・デメリット

  • メリット
    状態依存に従って親子関係を構築するので、状態変更時の無駄な再レンダリングの対策が自然と出来る。
  • デメリット
    状態依存に従って親子関係を構築するので、デザイン案に沿わない場合に工夫が必要になる。

さいごに

前編として、「今までのReactらしい状態管理術」の説明に重きを置き、コンポーネントツリーとファンクションファーストを取り上げました。

他にも実践向きの概念はいろいろあります。
(コンポーネントの共通となるstateを リフトアップ したり、状態管理術とUIを分離したりなど)

※本投稿に関するソースコードはこちらです( Next13のデモコード をベースにしています)。
https://github.com/vteacher-online/react-tree-demo/tree/master/app

後編へ:React Server Components 時代の状態管理術

ソースコードには React Server Components のデモもあります。
Next.js 13 から app ディレクトリが利用できるようになり、利用者や関連記事も増えてきました。

app ディレクトリは Meta(旧Facebook)がバンドルサイズ0を目指した新しいアプローチである React Server Components をベースにし、Vercel流にブラッシュアップしてきたものです。
(とはいえ、本家のMetaもその流れを汲んで 反映 しています)

※Nextの場合、デフォルトはサーバーコンポーネントです。

サーバーコンポーネントではuseStateもRecoilも使用できない

appディレクトリが本格的に導入されるとのことで、フロントエンジニアの方々は今までの状態管理術とはまた違ったやりかたを学ぶ必要が出てきました。

ご存知の通り、React Server Components はクライアントコンポーネントとサーバーコンポーネントに分かれます。useStateやRecoilなど、お馴染みの関数はクライアントコンポーネントのみ利用できます。サーバーコンポーネントでは利用できません。このあたりはいろいろ便利になってくるとは思いますが、React Server Components 時代の状態管理術はサーバーコンポーネントの扱いが肝となります。

React Server Components の基礎

初期(Metaのデモ)では、クライアントコンポーネントとサーバーコンポーネントを拡張子( .client.js.server.js )で分けていましたが、下記のようになりました。

  • use client を文頭に書くとクライアントコンポーネント
'use client'; // ←これ

export default function Hoge() {
  return (
    <>
      クライアント側(ブラウザ)でレンダリング
    </>
  );
}
  • 何も書かないと、サーバーコンポーネント(=デフォルト)
export default function Hoge() {
  return (
    <>
      サーバー側でレンダリング
    </>
  );
}

次回は【後編】を投稿します。

以上、Recoilおねえさんでした。

おまけ

おすすめの動画です。
https://www.youtube.com/watch?v=8pDqJVdNa44

おまけ2

レンダリングのお話です。

Discussion