shark-fin 21263-768x591

React Server Components を手軽に扱うフレームワーク react-server

react-server は Node.js で JavaScript ファイルを実行するかのように React Server Components を扱うことを目的としたフレームワークです。Next.js の機能が過剰に感じられるような小さなアプリケーションを開発する際に有用です。

2025 年 2 月現在 React Server Components を扱う方法として最も知られているのは Next.js を利用する方法でしょう。実際に Next.js は React Server Components が React の Canary の機能である段階で、すでに安定した機能として提供されていました。このため React Server Components が Next.js 固有の機能だと思われていたこともあったかもしれません。

Tip

React Canary とは安定版のバージョンがリリースされる前に React コミュニティに提供するリリースチャンネルです。Canary リリースでリリースされた機能には破壊的な変更が含まれるため、開発者が直接本番環境で使用することは想定されていません。Canary リリースの機能はフレームワークを介して使用して、フレームワークのレイヤーで破壊的変更を吸収して提供することが想定されています。Next.js は React Server Components に限らず Canary リリースの機能を積極的に取り入れているため、今後も React の新しい機能をいち早く利用できるフレームワークとして利点があります。

React Server Components を使用したい場合に Next.js を採用するのは確かに有効な選択肢です。しかし、小規模なアプリケーションを開発するような場合には Next.js の機能が過剰に感じられるかもしれません。例えばキャッシュやデータフェッチの仕様が複雑であったり、いざとなったら Next.js のソースコードを読み込んで理解する覚悟が必要といった意見が見受けられます。12

React Server Components を手軽に扱うためのフレームワークとして react-server が開発されました。このフレームワークの目的は、Node.js で JavaScript ファイルを実行するかのように React Server Components を扱うことです。react-server は Vite ベースのフレームワークであり、React Server Components だけでなく Server Actions もサポートされています。

この記事では react-server の基本的な使い方について紹介します。

プロジェクトを作成する

まずは react-server のプロジェクトを作成します。Node.js の v20.0.0 以上もしくは Bun v1.1.45 以上が必要です。Deno は 2025 年 2 月現在サポートされていません。

以下のコマンドを実行してプロジェクトを作成した後に、@lazarv/react-server をインストールします。reactreact-dom は react-server に含まれているため、個別にインストールする必要はありません。

mkdir my-react-server-app
cd my-react-server-app
npm init -y
npm install @lazarv/react-server

早速初めての React Server Component を作成してみましょう。src ディレクトリを作成し、その中に App.tsx ファイルを作成します。react-server は TypeScript をデフォルトでサポートしています。

src/App.tsx
import React from "react";
 
type Todo = {
  id: number;
  title: string;
  completed: boolean;
};
export default async function App() {
  const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  const data = (await res.json()) as Todo;
  return (
    <>
      <h1>Hello, React Server Components</h1>
      <p>{data.title}</p>
      <p>{data.completed ? "Completed" : "Not completed"}</p>
    </>
  );
}

以下のコマンドでサーバーを起動します。

npx react-server ./src/App.tsx

http://localhost:3000 にアクセスすると、Hello, React Server Components! と表示されるはずです。ファイルを編集すると HMR によりリアルタイムに変更が反映されます。

ビルドは以下のコマンドで行います。

npx react-server build ./src/App.tsx

ビルドが完了すると .react-server ディレクトリが作成され、その中にビルドされたファイルが格納されます。ビルドしたファイルは以下のコマンドで実行できます。

npx react-server start

Vite との統合

react-server は Vite の Environment API 上で構築されています。つまり react-server は多くの Vite の機能やプラグインといっしょに使うことができるのです。Vite で作成した React アプリケーションは簡単に react-server に移行できます。

まずは以下のコマンドで Vite のプロジェクトを作成します。

npm create vite my-react-server-app -- --template react-ts

続いて index.html の中身を src/main.tsx に移動します。

src/main.tsx
import App from "./App";
import "./index.css";
 
export default function Root() {
  return (
    <html lang="en">
      <head>
        <meta charSet="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/vite.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Vite + React + TS</title>
      </head>
      <body>
        <div id="root">
          <App />
        </div>
      </body>
    </html>
  );
}

App.tsx では useState() を使用しているため、"use client" ディレクティブを追加して Clietn Actions としてマークする必要があります。

src/App.tsx
"use client";
import { useState } from 'react'
import reactLogo from './assets/react.svg'

依存関係から reactreact-dom を削除し、@lazarv/react-server をインストールします。

npm uninstall react react-dom
npm install @lazarv/react-server

@larazv/react-server React の experimental バージョンを使用しているため、tsconfig.jsontypes フィールドに "react/experimental""react-dom/experimental" を追加する必要があります。

tsconfig.json
{
  "compilerOptions": {
    "types": ["react/experimental", "react-dom/experimental"],
  }
}

package.jsonscripts フィールド react-server を使うように変更します。

package.json
{
  "scripts": {
    "dev": "react-server ./src/main.tsx",
    "build": "react-server build ./src/main.tsx",
    "lint": "eslint .",
    "start": "react-server start"
  }
}

npm run dev でサーバーが起動できるかどうか確認してみましょう。

Vite のプラグインも試してみましょう。Tailwind CSS をインストールしてみます。Tailwind CSS は v4 から Vite のプラグインとしてシームレスに使用できるようになりました。

npm install tailwindcss @tailwindcss/vite

vite.config.ts を編集し、tailwindcss プラグインを追加します。

vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
 
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()]
})

index.csstailwindcss をインポートするように変更します。

index.css
@import "tailwindcss";

App.tsx で Tailwind CSS を使用してみましょう。

src/App.tsx
export default function App() {
  return (
    <div className="bg-blue-500 text-white p-4">
      <h1 className="text-2xl">Hello, React Server Components</h1>
      <p className="text-lg">This is a React Server Components app with Tailwind CSS!</p>
    </div>
  );
}

npm run dev でサーバーを起動し、http://localhost:3000 にアクセスしてみましょう。Tailwind CSS のスタイルが Vite のプラグインを通じて適用されていることが確認できるはずです。

フレームワークの設定

ビルドや開発時のオプションはコマンドライン引数もしくは設定ファイルにより設定できます。設定ファイルは react-server.config.{js,mjs,ts,mts.json} という名前でプロジェクトのルートディレクトリに配置します。また本番環境でのみ有効にしたい設定は react-server.production.config.{js,mjs,ts,mts,json} という名前で配置します。

Vite を利用している場合には vite.config.ts を設定の一部として使用できます。以下の例は開発サーバーのポートを 9999 に変更する例です。

vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
 
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
  port: 9999,
});

ルーティング

react-server のルーティングは <Route> コンポーネントを使用して定義する方法と、ファイルベースのルーティングを使用する方法の 2 つがあります。ここではファイルベースのルーティングを使用する方法を紹介します。

react-server ではコマンドを実行する際にエントリーポイントを指定しない場合、自動でファイルベースのルーティングを使用します。

npx react-server

TypeScript を使用している場合にはルーティングは自動で型安全になります。例えば <Link> コンポーネントの to プロパティに存在しないパスを指定した場合には型チェックによりエラーが発生します。

型チェックを有効にするためには tsconfig.jsoninclude フィールドに .react-server/**/*.ts を追加する必要があります。

tsconfig.json
{
  "include": [".react-server/**/*.ts"]
}

ファイルベースルーティングは Next.js のファイルベースルーティングと互換性があり、以下の機能をそのまま利用できます。

  • Static and dynamic routes
  • Layouts and pages
  • Nested routes
  • Route groups
  • Parallel routes
  • Error handling
  • Loading states

ルーティングの設定は react-server.config.ts もしくは vit.config.ts に記述します。root はルーティングの起点となるディレクトリを指定します。また public は静的ファイルを配置するディレクトリを指定します。

以下の設定例は Next.js と同じルールでルーティングを設定しています。

vite.config.ts
import { defineConfig } from "vite";
 
export default defineConfig({
  root: "app",
  page: {
    include: ["**/page.tsx"],
  },
  public: "public",
});

以下のディレクトリ構造では /, /about, /blog/[slug] ルートが生成されます。

app
├── about
│   └── page.tsx
├── blog
│   └── [slug]
│       └── page.tsx
└── page.tsx

page.tsx ファイルでは React コンポーネントを default エクスポートすることでページを定義します。パスパラメータは関数の引数として受け取ることができます。

app/blog/[slug]/page.tsx
type Params = {
  slug: string;
};
 
 
export default function BlogPage({ slug }: Params) {
  return <h1>Blog: {slug}</h1>;
}

layout.tsx ファイルを作成することで複数のページで共通するレイアウトを定義できます。layout.tsxchildren プロパティを受け取り、children はページのコンテンツを表します。

app/layout.tsx
export default function Layout({ children }) {
  return (
    <div>
      <header>Header</header>
      <main>{children}</main>
      <footer>Footer</footer>
    </div>
  );
}

ナビゲーションには @lazarv/react-server/navigation パッケージの Link コンポーネントを使用します。

app/page.tsx
import { Link } from "@lazarv/react-server/navigation";
 
export default function Home() {
  return (
    <div>
      <h1>Home</h1>
      <Link href="/about">About</Link>
      <Link href="/blog/hello">Blog</Link>
    </div>
  );
}

<Link> コンポーネントに prefetch プロパティを追加することでページのプリフェッチ有効にします。ユーザーがリンクにマウスでホバーした際にリンク先のページを事前に読み込むことで、ページ遷移時のパフォーマンスを向上させることが期待できます。

app/page.tsx
import { Link } from "@lazarv/react-server/navigation";
 
export default function Home() {
  return <Link to="/about" prefetch>About</Link>;
}

デフォルトではプリフェッチされたページの内容は無期限でキャッシュされます。prefetch プロパティの値としてキャッシュの有効期限をミリ秒単位で指定できます。

app/page.tsx
import { Link } from "@lazarv/react-server/navigation";
 
export default function Home() {
  // 5 秒後にキャッシュが破棄される
  return <Link to="/about" prefetch={5000}>About</Link>;
}

useClient フックの prefetch 関数を使用することで、任意のタイミングでページをプリフェッチできます。

app/page.tsx
"use client";
import { Link } from "@lazarv/react-server/navigation";
import { useClient } from "@lazarv/react-server/client";
 
export default function Home() {
  const { prefetch } = useClient();
  return (
    <div>
      <h1>Home</h1>
      <button onClick={() => prefetch("/about")}>Prefetch About Page</button>
      <Link to="/about">About</Link>
    </div>
  );
}

Static Generation

ページを静的な HTML として生成する場合にはページに一致するファイルを *.static.ts という名前で作成します。/about ページを静的に生成する場合には app/about/page.static.tsx というファイル名になります。

パスパラメータが含まれないページの場合は truedefault エクスポートすることで静的ページとして扱われます。

app/about/page.static.tsx
export default true;

パスパラメータが含まれるページの場合にはルートパラメータの配列を返すか、ルートパラメータの配列を返す非同期関数を default エクスポートします。

app/blog/[slug]/page.static.tsx
export default async function() {
  return [{ slug: "hello" }, { slug: "world" }];
}

キャッシュ

react-server では以下の 2 つのキャッシュがサポートされています。

  • レスポンスキャッシュ
  • in-memory キャッシュ

レスポンスキャッシュ

withCache 関数を使用することで Server Component のレスポンスをキャッシュできます。レスポンスキャッシュにはキャッシュプロバイダーと Cache-Control ヘッダーの stale-while-revalidate の両方が使用されます。サーバーサイドのキャッシュはキャッシュの有効期限が切れるまで後続のリクエストで再利用されます。クライアントサイドのキャッシュは同じクライアントからのリクエストで再利用されます。

app/page.tsx
import { withCache } from "@lazarv/react-server";
 
async function fetchData() {
  await new Promise((resolve) => setTimeout(resolve, 3000));
  const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  return await res.json();
}
 
export default withCache(async function Home() {
  const data = await fetchData();
  return (
    <div>
      <h1>Home</h1>
      <p>{data.title}</p>
    </div>
  );
}, 30 * 1000); // キャッシュの有効期限を指定する

withCache の代わりに useResponseCache フックを使用することもできます。

app/page.tsx
import { useResponseCache } from "@lazarv/react-server";
 
async function fetchData() {
  await new Promise((resolve) => setTimeout(resolve, 3000));
  const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  return await res.json();
}
 
export default function Home() {
  useResponseCache(30 * 1000);
  const data = await fetchData();
 
  return (
    <div>
      <h1>Home</h1>
      <p>{data.title}</p>
    </div>
  );
}

in-memory キャッシュ

useCache フックを使用することで in-memory キャッシュを使用できます。useCache フックは任意の非同期関数の結果をキャッシュします。キャッシュされた結果はすべての Server Component で共有されます。

app/page.tsx
import { useCache } from "@lazarv/react-server";
 
const id = 1;
 
async function fetchData() {
  await new Promise((resolve) => setTimeout(resolve, 3000));
  const res = await fetch("https://jsonplaceholder.typicode.com/todos/" + id);
  return await res.json();
}
 
export default async function Home() {
  const data = await useCache(["todo", id], fetchData, 30 * 1000);
 
  console.log(data);
 
  return (
    <div>
      <h1>Home</h1>
      <p>{data.title}</p>
    </div>
  );
}

フォームをサブミットした後などキャッシュを無効にしたい場合には revalidate 関数を使用します。revalidate 関数の引数には useCache で指定したキャッシュキーを指定します。

app/todo/[id]/page.tsx
import { revalidate } from "@lazarv/react-server";
 
export default function TodoPage({ id }) {
  async function action(formData) {
    const { title } = formData;
    await updateTodo(id, { title });
    await revalidate(["todo", id]);
    redirect("/");
  }
  return (
    <form
      action={action}
    >
      <input type="text" name="title" />
      <button type="submit">Submit</button>
    </form>
  );
}

"use cache" ディレクティブ

"useCache" フックを使用する代わりに "use cache" ディレクティブを使用することもできます。"use cache" ディレクティブを宣言した関数は in-memory にキャッシュされます。"use cache" ディレクティブには profile, key, ttl のプロパティを指定できます。

app/fetchData.ts
export async function fetchData() {
  "use cache ttl=200; tags=todos";
  await new Promise((resolve) => setTimeout(resolve, 3000));
  const res = await fetch("https://jsonplaceholder.typicode.com/todos");
  return await res.json();
}

profile は設定ファイルに定義されたキャッシュプロファイルを指定します。react-server.config.ts もしくは vite.config.ts にキャッシュプロファイルを定義できます。

vite.config.ts
{
  "cache": {
    "profiles": {
      "todos": { "ttl": 30000, "tags": "todos" }
    }
  }
}

定義したキャッシュプロファイルは以下のように使用できます。

app/fetchData.ts
export async function fetchData() {
  "use cache profile=todos";
  await new Promise((resolve) => setTimeout(resolve, 3000));
  const res = await fetch("https://jsonplaceholder.typicode.com/todos");
  return await res.json();
}

Adapter

デプロイ環境に合わせて最適なアプリケーションを構成するため Adapter を使用できます。現時点では以下の Adapter が提供されています。

  • @lazarv/react-server-adapter-vercel

以下の Adapter は現在開発中です。

  • @lazarv/react-server-adapter-netlify
  • @lazarv/react-server-adapter-cloudflare-pages
  • @lazarv/react-server-adapter-cloudflare-workers
  • @lazarv/react-server-adapter-sst

Adapter を使用するためにははじめにパッケージをインストールします。

npm install @lazarv/react-server-adapter-vercel

インストールした Adapter は react-server.config.ts もしくは vite.config.ts に設定します。

vite.config.ts
import { defineConfig } from "vite";
 
export default defineConfig({
  adapter: ["@lazarv/react-server-adapter-vercel", {
    // Adapter options
  }],
});

もしくはパッケージを import して設定します。

vite.config.ts
import { defineConfig } from "vite";
import vercelAdapter from "@lazarv/react-server-adapter-vercel";
 
export default defineConfig({
  adapter: vercelAdapter({
    // Adapter options
  }),
});

まとめ

  • react-server は React Server Components を手軽に扱うためのフレームワーク
  • npx react-server {entry-point} で開発サーバーを起動
  • npx react-server build {entry-point} でビルド
  • npx react-server start でビルドしたファイルを実行
  • Vite の Environment API を使用しているため、Vite の機能やプラグインを利用できる
  • 設定ファイルは react-server.config.ts もしくは vite.config.ts に記述
  • エントリーポイントを指定せずにコマンドを実行するとファイルベースのルーティングが使用される
    • ファイルベースは Next.js と互換性があり、多くの機能をそのまま利用できる
  • Static Generation は *.static.ts ファイルを使用する。パスパラメータが含まれない場合は true をエクスポート、含まれる場合はルートパラメータの配列もしくは配列を返す非同期関数をエクスポート
  • キャッシュにはレスポンスキャッシュと in-memory キャッシュがサポートされている
  • Adapter を使用することでデプロイ環境に合わせたアプリケーションを構成できる

参考

Footnotes

  1. You Don't Need Next.js

  2. [Next.js App Router での MPA フロントエンド刷新 - Speaker Deck]

記事の理解度チェック

以下の問題に答えて、記事の理解を深めましょう。

アプリケーションをビルドする際に使用するコマンドはどれか?

  • npx react-server ./src/App.tsx

    もう一度考えてみましょう

  • npx react-server build ./src/App.tsx

    正解!

  • npx vite ./src/App.tsx

    もう一度考えてみましょう

  • npx vite build ./src/App.tsx

    もう一度考えてみましょう

レスポンスキャッシュを有効にするために使われるフックはどれか?

  • useCache()

    もう一度考えてみましょう

  • useResponseCache()

    正解!

  • useCacheConsumer()

    もう一度考えてみましょう

  • useCacheContext()

    もう一度考えてみましょう


Contributors

> GitHub で修正を提案する
この記事をシェアする
はてなブックマークに追加

関連記事