React 19 Server Components: What Changed and Why

Deep dive into React 19 Server Components: Actions, use() hook, asset loading, optimistic updates, and the architectural patterns that make RSC production-ready.

E
ECOSIRE Research and Development Team
|2026年3月19日6 分で読める1.3k 語数|

Performance & Scalabilityシリーズの一部

完全ガイドを読む

React 19 サーバー コンポーネント: 変更点とその理由

React 19 は、フック以来最も重要なリリースです。 React 18 で実験的な機能として開始されたサーバー コンポーネントは、現在は安定しており、同時レンダリング モデルと完全に統合されています。しかし、その変更は単に RSC を安定させるだけではありません。React 19 では、アクション、新しい use() フック、フォーム統合、オプティミスティック更新、およびドキュメント メタデータ管理が導入されており、これらが集合的に React アプリケーションのデータ フローに対する考え方を変えます。

このガイドでは、React 18 と 19 の間で実際に変更された点に焦点を当て、各変更の背後にあるアーキテクチャ上の理由を説明し、古いアプローチに代わる生成パターンを示します。

重要なポイント

  • サーバー コンポーネントはサーバー上でのみ実行されます。ライフサイクル、状態、ブラウザ API はありません。
  • use() フックは、Promise とコンテキストを使用するためにクライアント コンポーネントの await を置き換えます
  • React 19 アクションは、フォーム送信用の手動の useState + fetch パターンを置き換えます
  • useOptimistic により、サーバー確認前の即時 UI 更新が有効になります
  • useFormStatus は、フォームコンポーネントをプロップドリルせずに保留状態を提供します
  • アセットのプリロード API (preloadpreinit) により、コンポーネントからのリソースのロードを制御できます
  • コンポーネント内の <title><meta>、および <link> タグは自動的に <head> にホイストされます。
  • サーバー コンポーネントとクライアント コンポーネントがツリーを形成 - クライアント コンポーネントはサーバー コンポーネントをインポートできません

サーバー コンポーネントの実際の内容

何が変更されたのかを説明する前に、サーバー コンポーネントとは何かを明確にしてください。サーバー上で排他的にレンダリングされ、クライアントがハイドレートする HTML + RSC ペイロードを生成する React コンポーネントです。これらは、サーバー側でレンダリングされるクライアント コンポーネントと同じではありません。

主な違い:

サーバーコンポーネントクライアントコンポーネント
実行日サーバーのみサーバー (初期レンダリング) + クライアント
フックを使用できますいいえはい
ブラウザ API を使用できるいいえはい
データベースにアクセスできますはい (直接)いいえ (API 経由)
バンドル サイズへの影響ゼロはい
非同期にすることもできますはいいいえ (サスペンスなし)
// Server Component — async, no hooks, direct DB access
async function UserProfile({ userId }: { userId: string }) {
  // Direct database query — no API needed
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
  });

  return (
    <div>
      <h1>{user.name}</h1>
      {/* Client Component gets data as props */}
      <UserActions userId={user.id} role={user.role} />
    </div>
  );
}
// Client Component — can use hooks, handles interactions
'use client';

function UserActions({ userId, role }: { userId: string; role: string }) {
  const [isEditing, setIsEditing] = useState(false);

  return (
    <div>
      <button onClick={() => setIsEditing(true)}>Edit</button>
      {isEditing && <EditUserForm userId={userId} />}
    </div>
  );
}

React 19 アクション

React 19 における人間工学上の最大の改善点はアクションです。これは、ユーザーの操作、特にフォームの送信によってトリガーされる非同期操作を処理する標準化された方法です。

React 19 より前では、フォーム処理は定型的な繰り返しでした。

// React 18 — manual state management
function ContactForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setSubmitting(true);
    setError(null);

    try {
      await createContact({ name, email });
    } catch (err) {
      setError(err.message);
    } finally {
      setSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* ... */}
    </form>
  );
}

React 19 アクションはこれを大幅に合理化します。

// React 19 — useActionState
'use client';

import { useActionState } from 'react';

async function createContactAction(
  prevState: { error?: string },
  formData: FormData
) {
  'use server'; // Server Action — runs on the server

  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  try {
    await createContact({ name, email });
    return { success: true };
  } catch (err) {
    return { error: err.message };
  }
}

function ContactForm() {
  const [state, formAction, isPending] = useActionState(
    createContactAction,
    {}
  );

  return (
    <form action={formAction}>
      <input name="name" required />
      <input name="email" type="email" required />
      {state.error && <p className="text-red-500">{state.error}</p>}
      <button type="submit" disabled={isPending}>
        {isPending ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

useActionState フックは、保留状態、フォーム データ、エラー処理を 1 か所で管理します。アクション関数の 'use server' ディレクティブは、アクション関数をサーバー アクションとしてマークします。アクション関数はサーバー上で実行され、データベースに直接アクセスでき、クライアントは実装を参照することはありません。


サーバーアクション

サーバー アクションは、クライアントから呼び出されたときにサーバー上で実行される、'use server' ディレクティブを備えた非同期関数です。これらは、ほとんどのデータ変更ユースケースの API ルートを置き換えます。

// app/actions/contacts.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@ecosire/db';
import { contacts } from '@ecosire/db/schema';

export async function createContact(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  if (!name || !email) {
    throw new Error('Name and email are required');
  }

  await db.insert(contacts).values({
    name,
    email,
    organizationId: await getOrganizationId(), // From session
  });

  revalidatePath('/dashboard/contacts'); // Invalidate cached page
  redirect('/dashboard/contacts'); // Redirect after success
}

サーバー アクションは form 要素で直接使用できます。

import { createContact } from '@/app/actions/contacts';

export default function NewContactPage() {
  return (
    <form action={createContact}>
      <input name="name" placeholder="Name" required />
      <input name="email" placeholder="Email" type="email" required />
      <button type="submit">Create Contact</button>
    </form>
  );
}

onSubmit ハンドラー、preventDefault()、手動 fetch() は必要ありません。フォームはそのまま機能します。


use() フック

React 19 では、レンダリング内でリソース (Promises と Context) を消費するための use() フックが導入されています。 await とは異なり、React のサスペンス システムで動作します。

// Before React 19 — had to resolve at the top level
export default async function Page() {
  const posts = await getPosts(); // Blocks entire page
  return <PostList posts={posts} />;
}
// React 19 — pass a promise, resolve with use()
export default function Page() {
  const postsPromise = getPosts(); // Start fetch immediately

  return (
    <Suspense fallback={<PostsSkeleton />}>
      <PostList postsPromise={postsPromise} />
    </Suspense>
  );
}

// Client Component using use() to consume the promise
'use client';

function PostList({ postsPromise }: { postsPromise: Promise<Post[]> }) {
  const posts = use(postsPromise); // Suspends until resolved

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

use() フックも useContext() を置き換えます。このフックは条件付きでコンテキストを消費できます (フックとは異なり、use() は条件およびループ内で呼び出すことができます)。

'use client';

import { use } from 'react';
import { ThemeContext } from '@/contexts/theme';

function Button({ children }: { children: React.ReactNode }) {
  const theme = use(ThemeContext); // Can be inside conditions

  return (
    <button className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'}>
      {children}
    </button>
  );
}

useOptimistic を使用したオプティミスティック更新

useOptimistic は、サーバー操作が完了する前にインスタント UI フィードバックを提供し、操作が失敗した場合は自動的に元に戻ります。

'use client';

import { useOptimistic, useTransition } from 'react';

interface Like {
  id: string;
  count: number;
  userLiked: boolean;
}

function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: Like }) {
  const [likes, setOptimisticLikes] = useOptimistic(
    initialLikes,
    (state, action: 'like' | 'unlike') => ({
      ...state,
      count: action === 'like' ? state.count + 1 : state.count - 1,
      userLiked: action === 'like',
    })
  );

  const [isPending, startTransition] = useTransition();

  async function toggleLike() {
    const action = likes.userLiked ? 'unlike' : 'like';

    startTransition(async () => {
      setOptimisticLikes(action); // Instant UI update

      // Server operation — if this fails, optimistic state reverts
      await togglePostLike(postId, action);
    });
  }

  return (
    <button onClick={toggleLike} disabled={isPending}>
      {likes.userLiked ? '♥' : '♡'} {likes.count}
    </button>
  );
}

オプティミスティック更新はすぐに表示され、その後コミット (サーバーの成功) または取り消し (サーバーの失敗) が行われます。ユーザーはネットワークの往復を待たずに即座にフィードバックを得ることができます。


コンポーネントからのドキュメントのメタデータ

React 19 では、<title><meta><link> タグを任意のコンポーネント内でレンダリングできます。これらは自動的にドキュメント <head> にホイストされます。

// Server Component — metadata hoists to <head> automatically
async function BlogPost({ slug }: { slug: string }) {
  const post = await getPost(slug);

  return (
    <article>
      <title>{post.title}</title>
      <meta name="description" content={post.description} />
      <link rel="canonical" href={`https://ecosire.com/blog/${slug}`} />

      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

これはサーバー コンポーネントとクライアント コンポーネントの両方で機能します。 Next.js のようなフレームワークでは、OpenGraph タグと hreflang 属性を完全に制御するために、引き続き generateMetadata() を使用します。React 19 のネイティブ サポートはより基本的です。ただし、単純な場合には、next/head または同様のライブラリは必要ありません。


アセット読み込み API

React 19 はリソースをプリロードするための明示的な API を提供し、リソースのロード ウォーターフォールをきめ細かく制御できます。

import { preload, preinit, prefetchDNS, preconnect } from 'react-dom';

function BelowFoldSection() {
  // When this component renders, preload the hero image for next section
  preload('/images/hero-next.webp', { as: 'image' });

  // Preinit a script (loads and executes immediately)
  preinit('https://cdn.example.com/analytics.js', { as: 'script' });

  // DNS prefetch for external resources
  prefetchDNS('https://fonts.googleapis.com');

  // Establish connection early
  preconnect('https://api.ecosire.com');

  return <section>{/* content */}</section>;
}

これらの API は、React のレンダリング モデルで正しく動作します。リソース ヒントをバッチ処理し、コンポーネント ツリーの奥深くからであっても、ドキュメント内の最適な位置でそれらを出力します。


よくある落とし穴と解決策

落とし穴 1: サーバー コンポーネントでフックを使用しようとする

サーバー コンポーネントは、useStateuseEffectuseContext、またはその他のフックを使用できません。これらが必要な場合は、コンポーネントに 'use client' を追加します。通常、エラーは Error: Hooks can only be called inside a function component です。

落とし穴 2: クライアント コンポーネントからサーバー コンポーネントをインポートする

クライアント コンポーネントはサーバー コンポーネントをインポートできません (逆は問題ありません)。これは、クライアント コンポーネントがサーバー コンポーネントが存在しないブラウザで実行されるためです。それらを構成する必要がある場合は、サーバー コンポーネントを children 小道具として渡します。

// Wrong — Client Component cannot import Server Component
'use client';
import { ServerUserProfile } from './server-user-profile'; // Error

// Correct — pass as children from a Server Component parent
// Parent (Server):
<ClientShell>
  <ServerUserProfile userId={userId} />
</ClientShell>

// ClientShell:
'use client';
function ClientShell({ children }: { children: React.ReactNode }) {
  return <div className="shell">{children}</div>;
}

落とし穴 3: プロパティをサーバーからクライアントに渡すときのシリアル化エラー

JSON シリアル化可能なデータのみが、小道具としてサーバーとクライアントの境界を越えることができます。関数、クラス インスタンス、Map、Set、および Dates (オブジェクトとして) はシリアル化できません。日付を ISO 文字列に変換します。関数を文字列識別子に置き換えます。

落とし穴 4: サーバー アクションが入力を検証しない

サーバー アクションでクライアントが提供するデータを決して信頼しないでください。データベースに書き込む前に、必ず Zod などで検証してください。

'use server';

import { z } from 'zod';

const schema = z.object({
  name: z.string().min(2).max(255),
  email: z.string().email(),
});

export async function createContact(formData: FormData) {
  const result = schema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
  });

  if (!result.success) {
    return { error: result.error.flatten().fieldErrors };
  }

  await db.insert(contacts).values(result.data);
}

よくある質問

サーバー コンポーネントは Next.js の外部で利用できますか?

React サーバー コンポーネントは React の機能ですが、サーバー レンダリング インフラストラクチャ (ルーティング、バンドル、ストリーミング) を処理するためのフレームワークが必要です。 Next.js App Router は、最も完成度の高い実装です。 Remix、Astro、および Vite ベースのセットアップに RSC サポートが追加されています。フレームワークなしで RSC を使用するには、大規模なカスタム インフラストラクチャ作業が必要です。

サーバー アクションは REST API ルートとどのように比較できますか?

サーバー アクションは、UI と同じ場所に配置されたミューテーションの方が簡単です。管理するエンドポイント URL、書き込みのための fetch() 呼び出し、エラー処理のボイラープレートは必要ありません。 REST API ルートは、Web アプリの外部から呼び出される操作 (モバイル アプリ、Webhook、サードパーティ統合)、ドキュメントが必要なパブリック API、および明示的な HTTP ステータス コードが必要な場合に適しています。ユースケースに基づいて、同じアプリケーションで両方を使用します。

サーバー コンポーネントのパフォーマンスへの影響は何ですか?

サーバー コンポーネントは、JavaScript バンドル サイズを削減し (コンポーネント コードがブラウザに送信されることはありません)、クライアント側のデータ取得ウォーターフォールを排除し、Suspense を介したストリーミング HTML 配信を可能にします。トレードオフはサーバーのコンピューティング コストです。レンダリングはクライアントのデバイスではなくサーバーで行われます。データ量の多いページの場合、これはほとんどの場合、最終的なメリットとなります。

コードベースに React 18 と React 19 を混在させることはできますか?

すべての React コードは React 19 として実行されます。ファイルごとのバージョン管理はありません。問題は、既存の React 18 コードが React 19 で動作するかどうかです。ほとんどの React 18 コードは変更されずに動作します。主な破壊的な変更は、refs (現在は通常の prop)、ReactDOM.render の削除、および @types/react のいくつかの型付けの変更です。自動移行のために React 19 codemod を実行します。

サーバー コンポーネントをテストするにはどうすればよいですか?

サーバー コンポーネントは、非同期 render を使用する React Testing Library でテストできます。サーバー アクションの場合は、模擬データベース呼び出しを使用してプレーンな非同期関数としてテストします。 Playwright を使用したエンドツーエンドのテストでは、特別なセットアップを行わずに、サーバー コンポーネントとクライアント コンポーネントの完全な統合がカバーされ、最終的な HTML 出力がテストされます。


次のステップ

React 19 サーバー コンポーネントは、最新の Web アプリケーションの構築方法における根本的な変化を表しています。 ECOSIRE のフロントエンド チームは、このアーキテクチャに基づいて運用アプリケーションを構築しました。サーバー コンポーネント、サーバー アクション、実際のユーザーのワークフローを強化するオプティミスティック アップデートを含む 249 ページです。

React 18 から 19 への移行、新しい RSC ファースト アプリケーションの設計についてサポートが必要な場合、またはチームにフロントエンド エンジニアリングの専門家が必要な場合は、弊社の開発サービスをご覧ください

E

執筆者

ECOSIRE Research and Development Team

ECOSIREでエンタープライズグレードのデジタル製品を開発。Odoo統合、eコマース自動化、AI搭載ビジネスソリューションに関するインサイトを共有しています。

WhatsAppでチャット