React 19新機能完全ガイド|useアクション・Server Components・新Hooks徹底解説


React 19で何が変わったのか

React 19に移行して最も衝撃を受けたのは、forwardRefが不要になったことです。何百回と書いてきたボイラープレートが消えました。refを普通のpropsとして渡せるだけで、コンポーネントの設計が驚くほどシンプルになります。

もう一つ、useOptimisticの登場でローディングスピナーの出番が激減しました。「いいね」ボタンを押した瞬間にUIが更新され、裏でAPIコールが走る — この当たり前のUXがReactの標準パターンになったのは大きな進歩です。

React 18で実験的だった多くの機能が正式に安定版となり、Reactアプリの設計パターンが根本的に変わります。

React 19の主要変更点一覧

カテゴリ新機能インパクト
ActionsuseActionState, useFormStatusフォーム処理が劇的に簡潔に
データ取得use() HookPromise/Contextの新しい読み方
楽観的更新useOptimisticUX向上のための標準パターン
Server ComponentsRSC正式対応サーバー/クライアント分離
ref改善refがpropsとして渡せるforwardRef不要に
メタデータ<title>, <meta>のネイティブサポートreact-helmet不要に
スタイルシート<link rel="stylesheet">の優先度制御CSS読み込み順の管理

Actions:フォーム処理の革命

従来のフォーム処理

React 18以前では、フォームの送信処理に多くのボイラープレートが必要でした:

// React 18以前の典型的なフォーム
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [isPending, setIsPending] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsPending(true);
    setError('');
    try {
      await login(email, password);
    } catch (err) {
      setError(err.message);
    } finally {
      setIsPending(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input value={password} onChange={e => setPassword(e.target.value)} />
      {error && <p className="error">{error}</p>}
      <button disabled={isPending}>
        {isPending ? '送信中...' : 'ログイン'}
      </button>
    </form>
  );
}

React 19のActions

// React 19のAction
function LoginForm() {
  const [state, submitAction, isPending] = useActionState(
    async (prevState: State, formData: FormData) => {
      const email = formData.get('email') as string;
      const password = formData.get('password') as string;
      try {
        await login(email, password);
        return { error: null, success: true };
      } catch (err) {
        return { error: err.message, success: false };
      }
    },
    { error: null, success: false }
  );

  return (
    <form action={submitAction}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      {state.error && <p className="error">{state.error}</p>}
      <SubmitButton />
    </form>
  );
}

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button disabled={pending}>
      {pending ? '送信中...' : 'ログイン'}
    </button>
  );
}

変わったポイント

  • useState × 3 → useActionState 1つに集約
  • e.preventDefault() が不要
  • isPending の管理が自動化
  • FormDataを直接受け取れる

useActionState:状態管理付きアクション

useActionStateは、アクションの結果を状態として管理するHookです。

const [state, action, isPending] = useActionState(
  async (previousState, formData) => {
    // アクションの処理
    return newState;
  },
  initialState
);

実践例:TODOアプリ

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

interface TodoState {
  todos: Todo[];
  error: string | null;
}

function TodoApp() {
  const [state, addTodo, isPending] = useActionState(
    async (prev: TodoState, formData: FormData) => {
      const text = formData.get('text') as string;
      if (!text.trim()) {
        return { ...prev, error: 'タスクを入力してください' };
      }

      try {
        const newTodo = await createTodo(text);
        return {
          todos: [...prev.todos, newTodo],
          error: null,
        };
      } catch (e) {
        return { ...prev, error: '追加に失敗しました' };
      }
    },
    { todos: [], error: null }
  );

  return (
    <div>
      <form action={addTodo}>
        <input name="text" placeholder="新しいタスク" />
        <button disabled={isPending}>
          {isPending ? '追加中...' : '追加'}
        </button>
      </form>
      {state.error && <p className="error">{state.error}</p>}
      <ul>
        {state.todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </div>
  );
}

useOptimistic:楽観的UI更新

useOptimisticは、サーバーの応答を待たずに即座にUIを更新するためのHookです。

function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (currentTodos, newTodoText: string) => [
      ...currentTodos,
      {
        id: 'temp-' + Date.now(),
        text: newTodoText,
        completed: false,
        sending: true, // 送信中フラグ
      },
    ]
  );

  async function handleAddTodo(formData: FormData) {
    const text = formData.get('text') as string;
    addOptimisticTodo(text); // 即座にUIに反映
    await createTodoOnServer(text); // サーバーに保存
  }

  return (
    <div>
      <form action={handleAddTodo}>
        <input name="text" />
        <button>追加</button>
      </form>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} style={{ opacity: todo.sending ? 0.5 : 1 }}>
            {todo.text}
            {todo.sending && ' (保存中...)'}
          </li>
        ))}
      </ul>
    </div>
  );
}

useOptimisticの動作フロー

1. ユーザーがフォーム送信
2. addOptimisticTodo() → UIが即座に更新(opacity: 0.5)
3. サーバーにリクエスト送信
4. サーバー応答後、実際のデータで再レンダリング(opacity: 1)
5. エラーの場合は楽観的更新がロールバック

use() Hook:Promise と Context の新しい読み方

Promiseの読み取り

use()はPromiseを直接読み取れるHookです。Suspenseと組み合わせて使います。

import { use, Suspense } from 'react';

// データ取得関数(Promiseを返す)
function fetchUser(id: string): Promise<User> {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // Promiseを直接読み取り

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  const userPromise = fetchUser('123'); // レンダリング時にPromise作成

  return (
    <Suspense fallback={<p>読み込み中...</p>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Contextの条件付き読み取り

use()は条件分岐内でも使える唯一のHookです:

function ThemeButton({ showTheme }: { showTheme: boolean }) {
  // 通常のuseContextは条件分岐内で使えないが、use()は使える
  if (showTheme) {
    const theme = use(ThemeContext);
    return <button className={theme}>テーマボタン</button>;
  }
  return <button>通常ボタン</button>;
}

refがpropsとして渡せるように

React 18以前:forwardRefが必要

// React 18 - forwardRefが必要
const FancyInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
  return <input ref={ref} className="fancy" {...props} />;
});

React 19:refは通常のprop

// React 19 - refは通常のpropsとして受け取れる
function FancyInput({ ref, ...props }: Props & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} className="fancy" {...props} />;
}

// 使用側
function Form() {
  const inputRef = useRef<HTMLInputElement>(null);
  return <FancyInput ref={inputRef} placeholder="入力" />;
}

forwardRefは非推奨となり、将来のバージョンで削除される予定です。


ドキュメントメタデータのネイティブサポート

React 19では、<title><meta><link>タグをコンポーネント内で直接レンダリングでき、自動的に<head>に配置されます。

function BlogPost({ post }: { post: Post }) {
  return (
    <article>
      {/* これらは自動的に<head>に移動される */}
      <title>{post.title} | TechBlog</title>
      <meta name="description" content={post.excerpt} />
      <meta property="og:title" content={post.title} />
      <link rel="canonical" href={`https://example.com/posts/${post.slug}`} />

      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

これにより、react-helmetnext/headに依存する必要がなくなります。


スタイルシートの優先度制御

function Component() {
  return (
    <>
      {/* precedenceでCSS読み込み順を制御 */}
      <link rel="stylesheet" href="/base.css" precedence="default" />
      <link rel="stylesheet" href="/theme.css" precedence="high" />
      <link rel="stylesheet" href="/component.css" precedence="default" />

      <div className="component">コンテンツ</div>
    </>
  );
}

precedence属性により、CSSの読み込み順序をReactが最適化します。


Server Components(RSC)

React 19でServer Componentsが正式に安定版になりました。

Server Componentの基本

// app/page.tsx - Server Component(デフォルト)
async function BlogPage() {
  // サーバーで直接DBアクセス可能
  const posts = await db.query('SELECT * FROM posts ORDER BY created_at DESC');

  return (
    <div>
      <h1>ブログ記事一覧</h1>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
      {/* Client Componentを含めることも可能 */}
      <SearchFilter />
    </div>
  );
}
// components/SearchFilter.tsx - Client Component
'use client';

import { useState } from 'react';

export function SearchFilter() {
  const [query, setQuery] = useState('');

  return (
    <input
      value={query}
      onChange={e => setQuery(e.target.value)}
      placeholder="記事を検索..."
    />
  );
}

Server ComponentとClient Componentの使い分け

用途Server ComponentClient Component
データ取得✅ 直接DB/APIアクセス❌ useEffectやSWR経由
状態管理❌ useState不可✅ useState/useReducer
イベントハンドラ❌ onClick不可✅ onClick等すべて
ブラウザAPI❌ window/document不可✅ 利用可能
バンドルサイズ✅ クライアントに送信されない❌ バンドルに含まれる
SEO✅ 完全なSSR△ ハイドレーション必要

Server Actions

Server Actionsは、クライアントから直接サーバーの関数を呼び出す仕組みです。

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

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // サーバーで直接DB操作
  const post = await db.insert('posts', { title, content });

  // キャッシュの再検証
  revalidatePath('/blog');

  return { success: true, id: post.id };
}
// app/new-post/page.tsx
import { createPost } from '../actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="タイトル" required />
      <textarea name="content" placeholder="内容" required />
      <button type="submit">投稿する</button>
    </form>
  );
}

React 19へのアップグレード手順

1. 依存関係の更新

npm install react@19 react-dom@19
npm install -D @types/react@19 @types/react-dom@19

2. 主な破壊的変更への対応

変更点対応
forwardRef 非推奨refをpropsとして受け取る
React.createContext のdefaultValue型がより厳密に
useRef に引数が必須useRef<T>(null) に統一
ReactDOM.render 削除createRoot を使用
string refs 削除callback ref or useRef を使用

3. Codemods(自動変換)

npx @react-codemod/v19 ./src

まとめ

React 19の主要な改善点:

  • Actions → フォーム処理のボイラープレートが激減
  • useOptimistic → 楽観的UIが標準パターンに
  • use() → データ取得がより宣言的に
  • ref改善 → forwardRefが不要に
  • メタデータ → react-helmet不要に
  • RSC正式対応 → パフォーマンスとSEOの向上

React 19は「書くコードが減り、パフォーマンスが上がる」アップデートです。特にActionsとServer Componentsの組み合わせは、フルスタックReactアプリの開発体験を大きく変えるでしょう。

関連記事