Open9

React

ピン留めされたアイテム
expsh13expsh13

use server

非同期 (async) 関数の本体の冒頭に 'use server'; を追加することで、その関数がクライアントから実行可能であるとマークします。そのような関数のことをサーバ関数 (Server Function) と呼びます。
クライアント上でサーバ関数を呼び出すと、渡された引数のシリアライズされたコピーを含んだネットワークリクエストがサーバに送信されます。サーバ関数が値を返す場合は、その値がシリアライズされてクライアントに返されます。

async function addToCart(data) {
  'use server';
  // ...
}

個々の関数に 'use server' をマークする代わりに、このディレクティブをファイルの先頭に追加することもできます。その場合はそのファイル内のすべてのエクスポートが、クライアントコードでインポートされる場合も含み、あらゆる場所で使用できるサーバアクションとしてマークされます。

  • 注意点
    • 内部で使用されるネットワーク呼び出しは常に非同期であるため、'use server' は非同期関数でのみ使用できます。
    • サーバ関数への引数は常に信頼できない入力として扱い、あらゆるデータ書き換えを検証してください。セキュリティに関する考慮事項を参照してください。
    • サーバ関数はトランジションの中で呼び出すようにしてください。サーバ関数が <form action> または formAction に渡される場合、自動的にトランジション内で呼び出されます。
    • サーバ関数は、サーバ側の状態を書き換える、更新目的のために設計されています。データの取得には推奨されません。(Next.jsのServer Actionsも同様。)したがって、サーバ関数を実装するフレームワークは通常、一度にひとつのアクションのみを処理し、返り値をキャッシュしないようにします。

使用法

  • フォームでサーバ関数を使用する
    サーバ関数の最も一般的なユースケースは、データを更新するための関数呼び出しです。React Server Components により、フォームに書かれたサーバ関数に対する第 1 級サポートが導入されます。
    この例では、requestUsername は <form> に渡されるサーバ関数となります。ユーザがこのフォームを送信すると、サーバ関数 requestUsername へのネットワークリクエストが発生します。フォーム内でサーバ関数を呼び出すとき、React はフォームの FormData をサーバ関数の最初の引数として提供します。
    フォームの action にサーバ関数を渡すことで、React によるフォームのプログレッシブエンハンスメント (progressive enhancement) が有効になります。つまり JavaScript バンドルがロードされる前にフォームを送信できるようになるということです。
async function requestUsername(formData) {
  'use server';
  const username = formData.get('username');
  // ...
}

export default function App() {
  return (
    <form action={requestUsername}>
      <input type="text" name="username" />
      <button type="submit">Request</button>
    </form>
  );
}
  • <form> の外部でサーバ関数を呼び出す
    サーバ関数とはサーバ側の公開エンドポイントとなるため、クライアントコードのどこからでも呼び出すことができます。
    フォームの外部でサーバ関数を使用する場合、トランジション内でサーバ関数を呼び出すようにしてください。これによりローディングインジケータを表示したり、楽観的に state 更新結果を表示したり、予期せぬエラーを処理したりすることができるようになります。フォームではサーバ関数は自動的にトランジション内にラップされます。
import incrementLike from './actions';
import { useState, useTransition } from 'react';

function LikeButton() {
  const [isPending, startTransition] = useTransition();
  const [likeCount, setLikeCount] = useState(0);

  const onClick = () => {
    startTransition(async () => {
      const currentCount = await incrementLike();
      setLikeCount(currentCount);
    });
  };

  return (
    <>
      <p>Total Likes: {likeCount}</p>
      <button onClick={onClick} disabled={isPending}>Like</button>;
    </>
  );
}
'use server';

let likeCount = 0;
export default async function incrementLike() {
  likeCount++;
  return likeCount;
}

https://ja.react.dev/reference/rsc/use-server

expsh13expsh13

Suspense

ローディングが終了したらそれに合わせて画面を書き換えなければいけません。つまり、fallbackの内容を片付けてサスペンドしたコンポーネントの本来の内容を表示するという作業が必要なはずです。サスペンド解除時はサスペンドしたSuspenseの中身が再レンダリングされる

下記のAlwaysSuspendは再レンダリングしたらまた新たなPromiseをthrowします。
これは、reactのSuspenseにてサスペンドされたAlwaysSuspendが解決した時再度描画されるため。

 const AlwaysSuspend: React.FC = () => {
   console.log("AlwaysSuspend is rendered");
   throw sleep(1000);
 };

https://zenn.dev/uhyo/books/react-concurrent-handson

expsh13expsh13

Transition

このようにstartTransitionの中で行われたステート更新はトランジションであると見なされます。トランジションは、端的に言えば優先度の低いステート更新です。

トランジションとしてマークされたステート更新は優先度が低いので、トランジション(としてマークされたステート更新によって引き起こされた再レンダリング)は中止されて一旦後回しにされる可能性があります。

const [counter, setCounter] = useState(0);
  return (
    <div className="text-center">
      <h1 className="text-2xl">React App!</h1>
      <Suspense fallback={<p>Loading...</p>}>
        <ShowData dataKey={counter} />
      </Suspense>
      <p>
        <button
          className="border p-1"
          onClick={() => {
            startTransition(() => {
              setCounter(counter + 1);
            });
          }}
        >
          Counter is {counter}
        </button>
      </p>
    </div>
  );

「Counter is 0」ボタンを押してみましょう。すると、直後は画面が何も反応しないはずです。1秒経つとShowDataの中身が「Data for 1 is …」に更新され、同時にボタンが「Counter is 1」になります。

つまり、ここでトランジションの結果として起こるレンダリングが遅延されたのです。遅延が起こった直接的な理由は言わずもがな、ShowDataがサスペンドしたことです。そして、トランジションの特異な点は、サスペンドの影響が<Suspense>の外まで漏れ出ている点にあります。というのも、counterはSuspenseの外にあるステートで、Suspenseの外でも使われているのに、そこのレンダリングも遅延されていますね。

https://zenn.dev/uhyo/books/react-concurrent-handson-2

expsh13expsh13

startTransition引数

scope: 1 つ以上の set 関数を呼び出して state を更新する関数。React は引数なしで直ちに scope を呼び出し、scope 関数呼び出し中に同期的にスケジュールされたすべての state 更新をトランジションとしてマークします。このような更新はノンブロッキングになり、不要なローディングインジケータを表示しないようになります。

https://ja.react.dev/reference/react/useTransition

expsh13expsh13

Server Actions

  • サーバアクションはトランジションの中で呼び出すようにしてください。サーバアクションが <form action> または formAction に渡される場合、自動的にトランジション内で呼び出されます。
  • サーバアクションは、サーバ側の状態を書き換える、更新目的のために設計されています。データの取得には推奨されません。したがって、サーバアクションを実装するフレームワークは通常、一度にひとつのアクションのみを処理し、返り値をキャッシュしないようにします。
import incrementLike from './actions';
import { useState, useTransition } from 'react';

function LikeButton() {
  const [isPending, startTransition] = useTransition();
  const [likeCount, setLikeCount] = useState(0);

  const onClick = () => {
    startTransition(async () => {
      const currentCount = await incrementLike();
      setLikeCount(currentCount);
    });
  };

  return (
    <>
      <p>Total Likes: {likeCount}</p>
      <button onClick={onClick} disabled={isPending}>Like</button>;
    </>
  );
}
// actions.js
'use server';

let likeCount = 0;
export default async function incrementLike() {
  likeCount++;
  return likeCount;
}

https://ja.react.dev/reference/rsc/use-server

expsh13expsh13

ハイドレーション

hydrateRoot を使用すると、react-dom/server によって事前生成した HTML コンテンツが含まれるブラウザ DOM ノード内に、React コンポーネントを表示できます。
domNode 内に存在する HTML にアタッチし、その内部の DOM の管理を引き継ぎます。

hydrateRoot(domNode, reactNode, options?)
  • 引数
    • domNode: サーバ上でルート要素としてレンダーされた DOM 要素。
    • reactNode: 既存の初期 HTML をレンダーするために使用された “React ノード”。これは通常、ReactDOM Server のメソッド(例:renderToPipeableStream(<App />))でレンダーされた JSX
  • 注意点
    • hydrateRoot() は、レンダーされたコンテンツがサーバでレンダーされたコンテンツと同一であることを期待しています。不一致はバグとして扱い修正する必要があります。

CSR の問題点は、"Interactive" の状態になるまでブラウザは真っ白であることです。
"Load JS" より手前で、事前に生成された HTML を表示します。
そうすることで、"Interactive" な状態まで真っ白な状態が続くことを避けることができます。

https://ja.react.dev/reference/react-dom/client/hydrateRoot#hydrating-server-rendered-html
https://zenn.dev/dozo13189/articles/07e96c182afa46

expsh13expsh13

use client

'use client' を使い、どのコードがクライアントで実行されるかをマークします。RSCにおける境界としての役割。

特徴

  • ファイルのトップに 'use client' を加えることで、当該モジュールとそれが連動してインポートしている依存モジュールがクライアントコードであるとマークします。'use client' でマークされているファイルがサーバコンポーネントからインポートされた場合、互換性のあるバンドラは当該モジュールのインポートを、サーバで実行されるコードとクライアントで実行されるコードの境界として扱います。クライアントで評価されるようマークされるコードとはコンポーネントに限りません。クライアントモジュールのサブツリーに含まれるすべてのコードは、クライアントに送信され、クライアントで実行されます。
'use client';

import { useState } from 'react';
import { formatDate } from './formatters';
import Button from './button';

export default function RichTextEditor({ timestamp, text }) {
  const date = formatDate(timestamp);
  // ...
  const editButton = <Button />;
  // ...
}

上記では formatDate と Button は RichTextEditor が依存するモジュールですので、これらのモジュール自体に 'use client' ディレクティブが含まれているかどうかに関わらず、これらもクライアントで評価されます。ある単一のモジュールが、サーバコードからインポートされた場合はサーバで、クライアントコードからインポートされた場合はクライアントで評価される場合があることに注意してください。

  • サーバで評価されるモジュールが 'use client' のモジュールから値をインポートする場合、その値は React コンポーネントであるか、またはクライアントコンポーネントに渡せるようサポート済のシリアライズ可能な props の型のいずれかでなければなりません。それ以外の方法で使用すると例外がスローされます。
client.tsx
'use client';

// シリアライズ不可能な値
export const clientOnlyFunction = () => {
  console.log('This runs only on the client!');
};

export const userData = {
  name: 'Alice',
  action: clientOnlyFunction, // シリアライズ不可能な関数
};
server.tsx
import { userData } from './clientData';

export default function ServerComponent() {
  // サーバで関数を含むオブジェクトを使おうとするとエラー
  console.log(userData.action); // エラー: シリアライズ不可能
  return <div>{userData.name}</div>;
}
  • コンポーネントは、サーバー環境から Props としてデータを受け取ることができる(RSC Payload にシリアライズされる。対応データ型限定)
server.tsx
// ServerComponent.js
import ClientComponent from './ClientComponent';

export default function ServerComponent() {
  const serverData = { message: 'Hello from the server!' };

  return (
    <div>
      <h1>Server Component</h1>
      {/* サーバーのデータを Props として渡す */}
      <ClientComponent {...serverData} />
    </div>
  );
}

https://ja.react.dev/reference/rsc/use-client
https://zenn.dev/yumemi_inc/articles/use-client-directive-explained-with-gssp
https://zenn.dev/uhyo/articles/react-server-components-multi-stage

expsh13expsh13

RSC

コンポーネント単位でのデータ取得&レンダリングをサーバー側で行うことができます。これは、SSR の問題点である「ページ単位という制限」を解決しています。

レンダリングフロー(初回表示〜2回目以降のページ遷移)

ソースがしっかりしてないので、アップデート必要かも。
Next.jsなどのフレームワークを使用する。

  1. 初回表示はクライアントからサーバーがリクエストを受け取ると、あらかじめ生成(SSG)またはリクエストを受け取り生成(SSR)したHTMLを生成。
    HTMLの生成は、まずRSCのServer Componentを実行して生成されたRSC ペイロードとClient Componentを元にして作成される。Client Componetはイベントなどは付与されいていないマークアップのみされた状態。
app/page.tsx
//  Server Component
import SomeClientComponent from "./SomeClientComponent";

export default function Page() {
  return (
    <div>
      <h1>This is a Server Component!</h1>
      <SomeClientComponent initialCount={3} />
    </div>
  );
}
app/SomeClientComponent.tsx
//  Client Component
"use client";

import { useState } from "react";

export default function SomeClientComponent({ initialCount }: { initialCount: number }) {
  const [count, setCount] = useState(initialCount);

  return (
    <button onClick={() => setCount((prev) => prev + 1)}>
      Count is: {count}
    </button>
  );
}

展開後のクライアントに返されるHTML
<html>
  <body>
    <div id="__next">
      <div>
        <h1>This is a Server Component!</h1>
        <div>
          <button>Count is: 3</button>
        </div>
      </div>
    </div>

    <!-- 以下、Next.js が挿入するスクリプト群 -->
    <script src="/_next/static/chunks/main.js" />
    ...
  </body>
</html>

  1. HTML、RSCペイロード、jsのバンドルがサーバーから返される。
  2. HTMLが返却されるので、ユーザーにいち早くコンテンツを表示できる。(ハードナビゲーション)ブラウザはロードしたHTMLを解析・描画するとともに、バンドルされたクライアントjsをロード。このバンドルにはServer Componentは含まれない。
  3. RSCペイロード、バンドルされたjsを元にReactツリーを構築し、それを元に仮想DOMを構築する。
  4. ReactDOM.hydrateRoot()などのハイドレーションのエントリーポイントを呼び出し、既存のDOMと仮想DOMの比較を行う。
  5. DOMの差分比較で問題なければハイドレーションを行う。
  6. 2回目以降のページ遷移時はHTMLのリクエストは行わず、必要な差分データのみリクエストし、サーバーでServer Componentが実行され、クライアントにRSCペイロード(Reactツリーがシリアライズされたもの)が返される。このとき、RSCペイロードのClient Componentは「ここに Client Component があるよ」というというマーカー情報が含まれます。必要なClient Componentがなければロードする。
リクエスト例
GET /articles/hello-world?__next_rsc=1&__next_route=/articles/[slug] HTTP/1.1
Host: example.com
Accept: */*
User-Agent: Mozilla/5.0 ...
レスポンス例
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

4f
[
  // RSC フライトデータの一部
  "$RSC", {
    "data": {
      "id": "hello-world",
      "title": "Hello World",
      "content": "...記事本文..."
    }
  }
]
0

https://ja.react.dev/reference/rsc/server-components
https://zenn.dev/uhyo/articles/react-server-components-multi-stage
https://zenn.dev/yuu104/articles/react-server-component?utm_source=chatgpt.com
https://chatgpt.com/share/6785d615-303c-800d-8b6e-f56ad672a7a5