cotoblog

React Server Actionについて

React Server Actionについて

プログラミング

この記事ではReact 19 RCで新たに追加されたServer Action機能と、それに関連するサーバコンポーネントおよびuseTransitionについて説明します。

  1. React サーバコンポーネントについて
  2. React Server Actionについて
  3. useTransitionフックについて


今回学習するServer Actionとは何でしょうか。公式サイトの冒頭では

Server Actionを使用することで、サーバで実行される非同期関数をクライアントコンポーネントから呼び出すことができます。

と記載されています。
クライアント側でその関数が呼び出されると、React はサーバーにリクエストを送り、サーバで関数を実行して、その結果をクライアントに返します。
この「サーバ」は、サーバコンポーネントそのものではなく、サーバーサイドで実行される非同期処理を指します。
そのServer Actionはサーバコンポーネント内でuse serverディレクティブを使用して定義します。
なお、Server ActionはReactの標準APIではなく、Reactエコシステム内の特定のフレームワークやアーキテクチャ(例えばNext.js)で導入された概念となります。
では、次にサーバコンポーネントについて説明します。

サーバコンポーネントについて

公式サイトの冒頭で

サーバコンポーネントは新しいタイプのコンポーネントです。クライアントアプリケーションや SSR サーバとは別の環境で、バンドル前に事前にレンダーされます。
サーバコンポーネントは、CIサーバでビルド時に一度だけ実行することも、ウェブサーバを使用してリクエストごとに実行することもできます。

と記載されています。
サーバコンポーネントはビルド時に実行され、ファイルシステムから読み取ったり静的なコンテンツをフェッチしたりすることが可能です。したがってウェブサーバは必須ではありません。たとえばコンテンツ管理システムから静的データを読み取ることもできます。
ビルド時に実行すると、コンポーネントが1度だけ実行され、その結果として生成された静的なHTMLが出力されます。
従来では、クライアントサイドでuseEffectを利用してAPIをフェッチしました。

import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)

function Page({page}) {
 const [content, setContent] = useState('');
 useEffect(() => {
  fetch(`/api/content/${page}`).then((data) => {
   setContent(data.content);
  });
 }, [page]);
  
 return <div>{sanitizeHtml(marked(content))}</div>;
}

ページの表示期間中に変化しない静的なコンテンツをただレンダーするためだけに、ユーザは 75K(gzip 後)のライブラリを追加でダウンロードしてパースし、さらにページのロード後に別のリクエストがデータをフェッチするのを待つ必要があります。
では、クライアントサイドで初期データをフェッチする際の問題点とは何でしょうか。

クライアントサイドで初期データををフェッチする際の問題点

  1. クライアントサイドでのAPIフェッチはクライアントサイドでデータを取得します。これはページがロードされた後に行われるため、初期のページ表示にはデータが含まれておらず、データがフェッチされるまでユーザーは待たなければなりません。
  2. クライアントサイドでの処理を行うために、Reactや関連ライブラリが必要です。これにより、ユーザーはページを表示するために75KB(gzip後)のJavaScriptライブラリをダウンロードし、パースする必要があります。このプロセスは、特に低帯域のネットワーク環境や性能が低いデバイスでのユーザー体験を悪化させる可能性があります。
  3. ページがロードされた後に別途APIリクエストを行い、データを取得する必要があるため、ユーザーはさらにそのデータが返されるのを待たなければなりません。このためユーザーが完全なコンテンツを閲覧するまでに時間がかかります。

サーバコンポーネントで初期データを取得

サーバコンポーネントを使用することで、これらのコンポーネントをビルド時に一度だけレンダーすることができます。

import marked from 'marked'; // Not included in bundle
import sanitizeHtml from 'sanitize-html'; // Not included in bundle

async function Page({page}) {
 // NOTE: loads *during* render, when the app is built.
 const content = await file.readFile(`${page}.md`);
 return <div>{sanitizeHtml(marked(content))}</div>;
}

このレンダー出力は、サーバサイドレンダリング (SSR) でHTMLに変換され、CDNにアップロードできます。
アプリがロードされる際、クライアントには元のPageコンポーネントの存在や、Markdownをレンダーするための高コストなライブラリの存在は見えません。レンダーされた出力が見えるだけです。
さらにコンテンツは最初のページロード時にすぐ表示され、静的コンテンツをレンダーするためだけの高コストなライブラリをバンドルに含める必要はありません。
サーバコンポーネントは、ページのリクエスト時にウェブサーバ内で実行することも可能であり、これにより API を構築することなくデータレイヤにアクセスできます。アプリケーションがバンドルされる前にレンダーされ、データとJSXをクライアントコンポーネントにpropsとして渡すことができます。

注意点

サーバコンポーネントはブラウザに送信されないため、useStateのようなインタラクティブなAPIを使用することができません。その他、サーバコンポーネントにインタラクティビティを追加するには、use clientディレクティブを使用してクライアントコンポーネントと組み合わせます。
よくある誤解として、サーバコンポーネントをuse serverを用いて定義するものだと誤解されます。サーバコンポーネントにはディレクティブがありません。use serverディレクティブは、サーバアクションのためのものです。

サーバコンポーネントでの非同期コンポーネント処理 

サーバコンポーネントにより、async/await を使用してコンポーネントを書くという新しい手法を用いることができます。
非同期コンポーネント内でawaitすると、Reactはサスペンドして、プロミスがresolveされるのを待ってからレンダーを再開します。サスペンスのストリーミングサポートのおかげで、これはサーバ/クライアントの境界をまたいで機能します。
なお、サーバでプロミスを作成し、それをクライアント側で待機することも可能です。
整理すると以下になります。

サーバサイドでのレンダリング

サーバがReactアプリケーションをレンダリングし、HTMLを生成します。
このとき、サスペンスを使っているコンポーネントが非同期データを待っている場合、Reactはそのコンポーネントのレンダリングを一時停止します。

ストリーミング

サーバは、データがすべて揃うのを待つのではなく、まず利用可能な部分からクライアントにストリーミングし始めます。これにより、クライアントはレンダリングされている部分をすぐに表示できるようになります。

サスペンドされたコンポーネントの解決

非同期データが解決されると、サーバはそのコンポーネントをレンダリングし、その結果をストリームに追加してクライアントに送信します。

クライアントサイドの結合

クライアントはサーバから送られてきた部分的なHTMLを受け取り、それを表示します。そして、サスペンドされたコンポーネントがresolveされると、クライアントサイドでそのコンポーネントが再レンダリングされ、表示が更新されます。

Server Actionについて

サーバアクションはサーバコンポーネント内で作成し、クライアントコンポーネントに props として渡すことができます。また、クライアントコンポーネントで直接インポートして使用することも可能です。
サーバアクションはサーバコンポーネント内で作成し、クライアントコンポーネントに props として渡します。

// Server Component
import Button from './Button';

function EmptyNote() {
 async function createNoteAction() {
  // 'use server' ディレクティブは関数の内部で最初に書いて、Server Actionを定義
  'use server';
  await db.notes.create();
 }

 return <Button onClick={createNoteAction}/>;
}

"use client";
export default function Button({onClick}) { 
 return <button onClick={onClick}>Create Empty Note</button>
}

use serverディレクティブは、関数の内部で最初に書く必要があります。
関数の外側に書いたり、他のコードの後に書いたりすると、その関数はServer Actionとして正しく定義されません。
したがって、use server';を関数内の最初に置くことを忘れないでください。

クライアントコンポーネントで直接インポートして使用する

クライアントコンポーネントでは "use server" ディレクティブを使用するサーバアクションファイルをインポートすることができます。

"use server";
export async function createNoteAction() {
 await db.notes.create();
}


"use client";
import {createNoteAction} from './actions';

function EmptyNote() {
 <button onClick={createNoteAction} />
}

さらに公式サイトでは「アクションとサーバアクションの組み合わせ 」方として言及されています。
サーバアクションはクライアントのアクションと組み合わせることができます。

"use server";
export async function updateName(name) {
 if (!name) {
  return {error: 'Name is required'};
 }
 await db.users.updateName(name);
}


"use client";
import {updateName} from './actions';

function UpdateName() {
 const [name, setName] = useState('');
 const [error, setError] = useState(null);
 const [isPending, startTransition] = useTransition();

 const submitAction = async () => {
  startTransition(async () => {
   const {error} = await updateName(name);
   if (!error) {
    setError(error);
   } else {
    setName('');
   }
  })
 }

 return (
  <form action={submitAction}>
   <input type="text" name="name" disabled={isPending}/>
   {state.error && <span>Failed: {state.error}</span>}
  </form>
 )
}

このようにクライアント側のアクションでラップすることで、サーバアクションによる isPending にアクセスできます。

useTransitionについて

公式サイト には

useTransition は、UI をブロックせずに state を更新するための React フックです。

と記載されています。
コンポーネントのトップレベルでuseTransitionを呼び出し、state 更新の一部をトランジションとしてマークします。useTransitionによって返されるstartTransition関数により、あるstate更新をトランジションとしてマークすることができます。
そのstartTransition関数の引数にServer Actionの非同期関数を処理します。
※React19で新たに非同期関数に対応。
useTransitionフックとServer Actionを組み合わせて、よりスムーズなユーザー体験を提供することが可能となります。
useTransitionは、非同期的な更新を低優先度で行うことができるReactのフックで、主にUIが応答性を保ちながら背景で非同期処理を実行する際に役立ちます。
Server Actionを利用してサーバーサイドの非同期処理を行う場合、その処理がユーザーの操作に応じてトリガーされることが多いですが、useTransitionを使うことで、このような非同期処理をユーザーインターフェースの優先度が低い部分に回すことができ、UIのスムーズさを維持することができます。

まとめ

いかがでしたでしょうか。
React Server Actionについての簡単な説明と、それを利用するにあたって必要な「React サーバコンポーネント」「useTransition」について整理してみました。