POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSXFacebook
Nadia Makarevich

本記事は、原著者の許諾のもとに翻訳・掲載しております。

React Server Actionsは、クライアントサイドのデータフェッチにおいて、fetchを置き換えることができるのでしょうか? 調査して確かめてみましょう。

React Server Actions(現在はServer Functionsとして知られています)をデータフェッチに使用することはできるのでしょうか? これは、時折話題に上がる非常に良い質問です(参考参考)。 しかし、Server Actionsを使ってデータを「フェッチ」するのは、本当に良いアイデアなのでしょうか? 「Actions(アクション)」という言葉は、それがフェッチ用ではないことをいくらか暗示しています。 では、なぜわざわざそれを検討する人がいるのでしょうか?

調べてみましょう! まずはそれらがそもそも何であるかを理解することから始め、メリットとデメリットに移り、最後にその質問に対する明確な答えを出して締めくくります。 いつもの「ケースバイケースですね」ではなく、今回は本当の答えをお約束します!

あ、念のため言っておきますが、ここで話しているのはクライアントでのフェッチについてのみです。 Server Componentsの場合、データをフェッチするためにActionsは必要ありません。 関数を直接インポートするだけで済みます。

一緒に進めるためのサンプルリポジトリはこちらです。

React Actions(Server Functions)とは何か

まずは認識を合わせるために、これらのActionsが何であるかについてから始めましょう。

Server Functions(2024年にServer Actionsから改名されました)は、開発者がフロントエンドのコンポーネントからバックエンドのコードを呼び出せるようにする、比較的新しいReactの機能です。 退屈なGET/POSTリクエストを介するのではなく、インポートした関数として直接呼び出すことができます。

一般的には次のようになります。 まず、Server Actionsを作成します。

// FILE: action.tsx
'use server';

export const saveSomething = async (data: string) => {
  console.log('SERVER: Action received with data:', data);

  return { success: true };
};

これは先頭に 'use server' と書かれた単なるファイルであり、利用可能な各アクションはこのファイルからのエクスポートにすぎません。 内部のコードはすべてサーバー側のコードです。

そして、インタラクティブなクライアントコンポーネントの中で、このアクションを通常のインポートのように呼び出します。

// FILE: submit-button.tsx
'use client';

// Just import that backend action like a normal import
import { saveSomething } from './action';
import { Button } from '@/components/ui/button';

export const ClientButton = () => {
  // Standard React onClick here
  const onClick = async () => {
     // call the action like a normal function and get the result
    const result = await saveSomething('Some data from client');
    console.log('CLIENT: Action result:', result);
  };

  return <Button onClick={onClick}>Click Me to save data</Button>;
};

これだけです。 これで、ボタンがサーバー上の処理を直接トリガーできるようになりました。

このようにReactでバックエンドを書くのが良いアイデアかどうかは、まったく別の、おそらく議論が白熱するテーマです。 今日は、この魔法がどのように機能するかに焦点を当てましょう。

サンプルのリポジトリをまだダウンロードしていない場合は、今がその時ですsrc/app/simple-action-example ディレクトリが必要です。 開発モードでコードを起動し、/simple-action-example のURLを開くと、まったく同じボタンが表示されます。

Chromeのコンソールを開き、ボタンを数回クリックします。 すると次のように表示されます。

ここには「クライアント」のログのみが表示されます。 サーバーログが表示されているターミナルウィンドウを開くと、次のように表示されるはずです。

「サーバー」部分がここにあります。 これはなかなかクールですね。 コードはすっきりしていますし、最も重要なのは、ここで完全な型安全性が確保されており、データへの信頼性やIDEでのオートコンプリートといったあらゆる恩恵を受けられることです。

ここには、すぐに目に留まるはずの重要なことがもう一つあります。 いや、私がダークモードを嫌いという事実ではありません😅。 これです:

「サーバー」ログと一緒に、Actionの呼び出しをトリガーしたURLへのPOSTリクエストが表示されているのがわかります。 ネットワークパネルを開くと、それが確認できます。

繰り返しますが、魔法は存在しません。

私たちのActionsは、単なるPOSTリクエストにすぎません。 いずれかをクリックすると、saveSomething 関数の引数として送信した正確なペイロードを確認できます。

そして「レスポンス」をクリックすると、視覚的には少し崩れていますが、クライアントでログに記録しているのとまったく同じレスポンスが得られます。

このレスポンスは、React Server Components (RSC) ペイロードとして知られているものです。 そして、そのフォーマットはここではまったく関係ありません。 重要なのは、それが関数から返された通常の型付きデータであるかのように、console.log で出力できたということです。

// I can just console.log "result.success" as if it's a normal import
const onClick = async () => {
    const result = await saveSomething('Some data from client');
    // completely type safe
    console.log('CLIENT: Action result:', result.success);
  };

控えめに言って、これは本当にクールですね。

データフェッチのためのReact Server Actions:方法と理由

さて、Server Actionsが本質的にはPOSTリクエストの気の利いたラッパーにすぎないことがわかりました。

ここで質問です。 レスポンスを安全に読み取れることを考慮すると、その気の利いたラッパーを使ってデータを「フェッチ」することを妨げているのは一体何でしょうか? こんなことはできるのでしょうか?

useEffect(() => {
  const fetchProducts = async () => {
	// can this be a Server Action?
    const data = await getProducts();
    setProducts(data);
    setLoading(false);
  };

  fetchProducts();
}, []);

答えは、おそらくすでにお察しの通り、もちろん「できる」です。

たしかに裏側ではPOSTエンドポイントになりますが、それがどうしたと言うのでしょうか? GraphQLは何年もの間、データフェッチにPOSTリクエストを使用してきましたし、それらは長らくデータフェッチの特効薬と考えられてきました。

それに、メリットを考えてみてください!

手動での JSON.parse() はもう必要ありません。 Zodを使ってレスポンスに型の安全性を持たせようと試みる必要もありません。 IDEのオートコンプリートを通じて、バックエンドでどのようなデータが利用可能かを常に把握できます!

さらに重要なことに、データの操作にすでにActionsを使用している場合、データをフェッチするために「従来型」の複雑なエンドポイントを作成することは全く意味がありません

実際のところ、私が知っているメリットはこれですべてです。 しかし、開発体験がより良くなるのは間違いありませんし、GETの代わりにPOSTリクエストになるだけのコストであれば、やらない理由はないでしょう?

データフェッチにActionsを使用することへの一般的な反論

では、データフェッチにActionsを使用することへの反論にはどのようなものがあるのでしょうか? 最も一般的なのは「そのために設計されたものではない」という意見です。 私の良き友人であるClaudeにこの質問を投げかけてみたところ、一番最初に返ってきたのがこの回答でした。 これは、それが実際に最も一般的な意見であることをある意味証明しています。

インターネット上や、おしゃべりな友人たちの回答でよく見かけるその他の共通事項は次のとおりです。

  • GETとは異なりPOSTであるため、HTTPヘッダーを介したキャッシュが行われない。
  • fetchとは異なり、リクエストのメモ化が行われない。

これらを一つずつ見ていきましょう。

Actionsはデータフェッチ用に設計されていない

「そのために設計されていない」という主張は、もはや議論ですらありません。 それは宗教的・哲学的な論争であり、勝ち目はありません。 そして私の中では、POSTリクエストがGraphQLエコシステム全体やApolloのようなフレームワークにとって十分優れているのであれば、私にとっても十分に優れていると言えます。 もちろん、哲学的な観点からの話ですが。

したがって、もしこの「概念的」な議論がActionsに対する唯一の反対意見であるなら、私は迷わずActionsを使います。 最近の私はとても現実主義なのです。

HTTPヘッダーによるキャッシュがない

これは確かに事実です。 (すべてではないにせよ)ほとんどのブラウザは、適切なヘッダーが設定されていたとしてもPOSTリクエストをキャッシュしません。 ご自身で確認したい場合は、こちらの興味深い実験をいくつか参照してください。

しかし!

これはブラウザとCDNにのみ適用される話です。 キャッシュの動作をCache-Control ヘッダーに依存しているのはこれらです。 したがって、データリクエストがそこに保存されることを想定していないのであれば、気にする必要はありません。 たとえば、ログイン後にアクセスされるほとんどのリクエストがこれに該当します。 また、クライアントサイドキャッシュのように、リクエストではなくデータ自体をキャッシュするタイプのキャッシュ機構であれば、まったく問題ありません。

リクエストのメモ化が行われない

ああ、これは面白いですね! なぜなら、技術的には事実ですが、ここでは完全に見当違いだからです。

ここで私たちが議論しているのはクライアントサイドのフェッチであり、リクエストのメモ化サーバーの機能だからです。 クライアントで明示的にデータをキャッシュする優れたデータフェッチライブラリを使用していない限り、useEffect 内の fetch リクエストをメモ化しようとする人はいません。

そして、何らかの優れたデータフェッチライブラリを使用する場合、それがActionであろうと fetch であろうと関係なくキャッシュは行われます。 ライブラリ独自のルールに従って、返されたデータをメモリに保存するだけです。

したがって、従来の fetch の代わりにActionsを使用することは、その観点からは全く同じです。

大規模なアプリでActionsを試してみよう

さて、私がログインページの後ろにあるアプリを開発していて、クライアントサイドでのデータフェッチを行いたいと仮定しましょう。 この場合、fetch の代わりにServer Actionsを使うことに対する私の唯一の反論は「概念的」なものであり、実用上の欠点はないということでしょうか?

はっ! もしそうなら、明日からすぐに使ってみます。 メリットしかなく、デメリットがないのですから。

しかし、私たちは今日ここで比較と調査のために集まったわけですから、もう一度パフォーマンス探偵🕵🏻‍♀️の真似事でもしてみましょう。

この調査のために、各ブロックのデータをRESTエンドポイントからフェッチする「ダッシュボード」アプリを実装しました。 なかなか良い出来です。

RESTエンドポイントは合計で7つあります。

  • カードの最上列用のデータをフェッチするSummaryエンドポイント。
  • 4つのチャート用のデータをフェッチする3つのエンドポイント(なぜ3つなのかは聞かないでください)。
  • チャートの下にある2つのテーブル用の2つのエンドポイント。
  • 右側の「ステータス」データをフェッチする1つのエンドポイント。

これらはすべてTanStack Queryで実装されており、より洗練された、実際の私のやり方に近い形になっています。

アプリ自体はだいたいこんな感じです。

<>
  <DashboardHeader />
  <MetricsCards />
  <DashboardGrid>
    <OrdersLineChart />
    <RevenueAreaChart />
    <SalesByProductBarChart />
    <CustomerDistributionPie />
    <OrdersTable />
    <ProductsTable />
  </DashboardGrid>
</div>

{/* Right Sidebar */}
<div className="flex-shrink-0">
  <StatusSidebar />
</div>

リモートデータを必要とする各コンポーネントは、次のような構成になっています。

export function OrdersLineChart() {
  const { data, isLoading, error } = useQuery({
      queryKey: ['time-series'],
      queryFn: fetchData,
    })

  if (isLoading) return <OrdersLineChartLoading />;

  if (error) return <OrdersLineChartErrorComponent />;

  // data goes here
  return <Card>...</Card>

ここで、fetchData 関数が実際の fetch を行う場所です。

async function fetchData() {
  const response = await fetch('/api/time-series');
  const data = await response.json();

  // validating response data with zod
  return TimeSeriesDataArraySchema.parse(data);
}

今日は型安全性が大きなテーマなので、すべてのレスポンスをZodでバリデーションまでしています。

学習用サンプルのリポジトリでは、ルートの page.tsx コンポーネント(src/app/page.tsx)を開き、そこから実装を追跡してください。

今回の実験では、初めての訪問者のための初回ロード、つまりキャッシュがない状態に焦点を当てています。 「Largest Contentful Paint」の略であり、ページのメインタイトルが表示されるタイミングに相当するLCPの数値を測定したいと考えています。 さらに、すべてのデータが表示されるタイミング、つまり最後のデータフェッチが完了するタイミングにも関心があります。

そして最後に、数値を再現したくてChromeを使用している場合は、並列リクエストのためにHTTP/2を模倣するCaddyが必要です。 ChromeにはHTTP/1の並列接続数に制限があり、それはわずか6つだからです。

これらのことやその測定方法について全くわからない場合、私はこのテーマに関する詳細な記事をたくさん書いており、それだけに焦点を当てた「Web Performance Fundamentals」という本も出版しています。

4Gインターネットのスロットリングを適用した場合のパフォーマンスの状況は次のとおりです。

これは典型的なクライアントサイドのデータフェッチです。 最初にJavaScriptがダウンロードされ、次にいくつかのリクエストがトリガーされ(今回のケースではReact内のTanStackによる)、データが到着するまでしばらくの間、ローディングスケルトンを眺めることになります。

関心のある数値は次のようになります。

シンプルなfetch
LCP (タイトル表示) 500ms
全データ表示 1.7s

Actionsの影響(もしあれば)を測定するには、すべての fetchData 関数の内容を置き換えるだけです。 ここの fetch とZodを取り除く必要があります。

async function fetchData() {
  const response = await fetch('/api/time-series');
  const data = await response.json();

  // validating response data with zod
  return TimeSeriesDataArraySchema.parse(data);
}

そして、それらをすべて次のような単一の関数呼び出しに置き換えます。

import { getTimeSeriesData } from '@/actions/timeSeries';

async function fetchData() {
  return getTimeSeriesData();
}

Next.jsがそれらの呼び出しをActionsに変換する処理を引き受け、TanStackがクライアントサイドのキャッシュを引き受け、Actionsが型付けを引き受けてくれます。 学習用サンプルのリポジトリで、src/queries ディレクトリを開き、すべてのクエリ内のfetch関連コードをすべて削除して、必要なActionのコメントアウトを解除してください。

よし、すべてを置き換えてウェブサイトをリビルドし(パフォーマンスの測定は常に本番ビルドで行うことを忘れないでください!)、最終的な記録がこちらです。

あれ?🤔

これは一体?😱

並列リクエストがすべて直列になってしまったのでしょうか?🤔 控えめに言っても、これは良くないですね。 おまけとして、数値を比較してみます。

シンプルなfetch Actions経由のfetch
LCP (タイトル表示) 500ms 500ms
全データ表示 1.7s 8s

なぜ通常のPOSTリクエストがこのような奇妙なものに変わってしまったのでしょうか? きっとバグに違いありません!

いいえ。 実は、Reactのドキュメントを注意深く読めば、これは文書化された動作であることがわかります。 とても、とても注意深く読めば、ですが。 use server のドキュメントの中にある、他のすべての注意点(caveats)の中に隠れるように、次のように書かれています。 「したがって、Server Functionsを実装するフレームワークは、通常、一度に1つのアクションを処理します...」

フレームワークのドキュメントでも、「データの更新」ページの真ん中にある小さな「Good to know(知っておくべきこと)」ブロックの中で明確に文書化されています

これが、Server Actionsがデータフェッチ用に「設計されていない」理由です! 知っておいて損はありません。

データフェッチにActionsを使用する際のもう一つの注意点はデバッグです。 ネットワークパネルをよく見ると、localhost エンドポイントへの呼び出しが大量にあることに気づくでしょう。 それらのデータフェッチはすべて同じ名前になっています! これは上のスクリーンショットでも確認できます。 Responseタブの崩れたRFCフォーマットと相まって、デバッグ体験としてはかなり悲惨なものになります。

しかし、とにかくデータフェッチにはServer Componentsを使うべきだ!

Reactコミュニティの一部には、クライアントでのデータフェッチはすでに過去の遺物であり、常にServer Componentsを使用するべきだという強い意見があります。

さて、そのトピックに対して避けられないであろう大量のコメントに対処するための、私の強い意見はこうです。 私たちは「絶対にそうすべきではない」ということです。

断言しますが、クライアントサイドのフェッチは、すぐにどこかに消えてしまうようなものではありません。 Server Componentsよりもクライアントサイドのフェッチを好む理由は、文字通り数え切れないほどあります。

私たちがすべきことは、基本を学ぶことです。 すなわち、サーバーサイドレンダリング(SSR)がどのように機能するのか、SSRとServer Componentsの違いは何なのか、そしてそれらすべてを念頭に置いた上でどのように・なぜデータをフェッチするのか、ということです。 これらすべてをどのように測定するのかを知っておいて損はありません。 そして当然、データフェッチのためのいくつかの優れたツールを自らの武器として持っておく必要があります。

なぜなら、そうすれば、データフェッチにSSRやServer Componentsを導入することが正確に何をもたらし、代わりに何を要求するのかを、事実と数字に基づいて理解できるようになるからです。 たとえば、この特定のアプリにおいて、SSRを導入すると、コストをかけずに全体的なスコアが向上し、ローディングスケルトンを排除できます。 しかし、LCPは2倍悪化します。 これをServer Componentsで書き直すにはより多くのリファクタリングが必要になりますし、比較してパフォーマンスが向上する可能性はありますが、ローディングスケルトンは戻ってきますし、アプリの保守や理解がはるかに困難になります。

ちなみに、仮にそれを実行した場合の数値は以下のようになります。 異なるレンダリング手法についてさらに調査したい方は参考にしてください。

シンプルなfetch Actions経由のfetch SSR Server Components
LCP (タイトル表示) 500ms 500ms 1.3s 520ms
全データ表示 1.7s 8s 1.3s 1.2s

サンプルリポジトリの /dashboard-with-ssr ルートと /dashboard-with-server-components ルートが必要です。

とりあえず、データフェッチにおけるActionsの調査はこれで終わりにしましょう。

まとめ(TL;DR)

技術的には、はい、React Server Actionsを使用してクライアントでデータをフェッチすることは可能です。

しかし、後生ですから、絶対にやめてください。 複数のリクエストを並列で送信する必要が生じた瞬間に、あるいは返されたデータをデバッグしようとした瞬間に、後悔することになります。

クライアントでデータをフェッチする必要がある場合は、TanStack Query(または同様のライブラリ)と組み合わせて、適切でシンプルなRESTリクエストを使用してください。

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。