React 19 の衝撃は一言で表すと「手で書いてきた最適化とフォーム周りのボイラープレートが、React 側に吸収された」です。useMemo / useCallback を書く判断が消え、isPending / error ステートを自分で管理する必要がなくなり、forwardRef すら廃止予定になりました。2025 年 10 月には React Compiler 1.0 が安定版となり、React 19.2 で <Activity /> コンポーネントや useEffectEvent が導入され、流れは完全に固まっています。
この記事は 2026 年 4 月時点の React 19.2 + React Compiler 1.0 を前提に、新機能を順に採用していけば必ず恩恵が出る実戦パターンとして整理しました。Actions / useActionState / useFormStatus / useOptimistic による非同期フォーム処理、use() フック、ref as prop、Document Metadata、Resource Preloading、Server Components / Actions、19.2 の <Activity /> と useEffectEvent、移行手順・落とし穴まで、TypeScript 実例で一気通貫に解説します。
- 2026 年 4 月時点のバージョン整理
- React Compiler 1.0 ── 手動メモ化からの解放
- Actions ── 非同期フォームのパラダイム転換
- useFormStatus ── 子コンポーネントで送信状態を知る
- useOptimistic ── 楽観的 UI を宣言的に
- use() フック ── Promise と Context を render 内で読む
- ref as prop ── forwardRef の廃止
- <Context> as provider ── Context.Provider の簡略化
- Document Metadata ── コンポーネント内で <title> / <meta> を書く
- Stylesheet precedence ── 読み込み順をコンポーネントで宣言
- Resource Preloading APIs ── パフォーマンスを宣言的に上げる
- Server Components と Server Actions(安定化)
- React 19.2 の目玉 ── <Activity /> と useEffectEvent
- React 18 → 19 移行ガイド
- 落とし穴と注意点
- よくある質問
- まとめ
2026 年 4 月時点のバージョン整理
React 19 周りは短期間に重要リリースが続いたため、まず前提を揃えます。
| リリース | 時期 | 主な追加 |
|---|---|---|
| React 19.0 | 2024 年 12 月 | Actions / useActionState / useFormStatus / useOptimistic / use() / ref as prop / Server Components 安定化 / Document Metadata / Stylesheet precedence / Resource Preloading / Custom Elements |
| React Compiler 1.0 | 2025 年 10 月 | 自動メモ化が本番利用可能に。React 17 以降互換 |
| React 19.2 | 2025 年 10 月 | <Activity /> コンポーネント・useEffectEvent・cacheSignal・View Transitions 強化・Performance Tracks(Chrome DevTools 連携)・SSR Batching |
| 推奨バージョン | 2026 年 4 月 | React 19.2 + React Compiler 1.0(新規プロジェクトはこの組合せで) |
React Compiler 1.0 ── 手動メモ化からの解放
React Compiler は「コンポーネントとフックを自動で最適化する」コンパイラです。従来 useMemo / useCallback / React.memo で手書きしていた再レンダリング抑止が、ビルド時にコードを書き換える形で自動化されます。条件分岐内の値・クロージャも正しくメモ化でき、手書きでは追えない粒度の最適化が可能です。
function ProductList({ products, onSelect }: Props) {
const expensive = products.map((p) => transform(p)); // 毎回再計算
const handleClick = (id: string) => onSelect(id); // 毎回新しい関数
return (
<ul>
{expensive.map((p) => (
<Item key={p.id} data={p} onClick={handleClick} />
))}
</ul>
);
}
import { useMemo, useCallback, memo } from "react";
const ProductList = ({ products, onSelect }: Props) => {
const expensive = useMemo(() => products.map(transform), [products]);
const handleClick = useCallback((id: string) => onSelect(id), [onSelect]);
return (
<ul>
{expensive.map((p) => (
<Item key={p.id} data={p} onClick={handleClick} />
))}
</ul>
);
};
const Item = memo(function Item({ data, onClick }: ItemProps) { ... });
インストールと設定(Vite 7 / Next.js 15)
npm install --save-dev babel-plugin-react-compiler eslint-plugin-react-compiler npm install react@latest react-dom@latest
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
react({
babel: {
plugins: [["babel-plugin-react-compiler", { target: "19" }]],
},
}),
],
});
next.config.ts で experimental.reactCompiler = true を設定するだけで Compiler が有効化されます。Next.js の詳細は Claude Code × Next.js フルスタック開発完全ガイド を参照してください。ESLint プラグインで Rules of React 違反を検知
{
"plugins": ["react-compiler"],
"rules": {
"react-compiler/react-compiler": "error"
}
}
Actions ── 非同期フォームのパラダイム転換
form 要素の action 属性に関数を渡すだけで、保留中(pending)状態・エラー・送信後の自動リセットまで React が管理します。従来の「useState で isLoading を管理 → try/catch でエラー → 成功時にフォームをクリア」というボイラープレートが一掃されます。
function UpdateNameForm() {
const [name, setName] = useState("");
const [error, setError] = useState<string | null>(null);
const [isPending, setBusy] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setBusy(true); setError(null);
try {
await updateName(name);
setName(""); // フォームリセット手動
} catch (err) {
setError(String(err));
} finally {
setBusy(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button disabled={isPending}>更新</button>
{error && <p>{error}</p>}
</form>
);
}
import { useActionState } from "react";
async function updateAction(_prev: string | null, formData: FormData) {
const name = formData.get("name") as string;
try {
await updateName(name);
return null; // 成功
} catch (e) {
return String(e); // エラーを state に反映
}
}
export function UpdateNameForm() {
const [error, submitAction, isPending] = useActionState(updateAction, null);
return (
<form action={submitAction}>
<input name="name" required />
<button disabled={isPending}>更新</button>
{error && <p>{error}</p>}
</form>
);
}
useState による isPending 管理が消えた ② try/catch のエラー処理が純関数のリターンで表現できる ③ 成功時のフォームリセットは React が自動で行う(non-controlled input の場合)。大規模アプリで同じパターンを何十回書いていたなら、コード量の体感で 40〜60% 減ります。useFormStatus ── 子コンポーネントで送信状態を知る
送信ボタンを独立コンポーネントに切り出したいが、親の isPending をプロップドリルしたくない——そんな場面で使うのが useFormStatus です。直近の親フォームの送信状態を、ボタン側で直接取得できます。
"use client";
import { useFormStatus } from "react-dom";
export function SubmitButton({ children }: { children: React.ReactNode }) {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "送信中..." : children}
</button>
);
}
// 使う側は isPending を渡さなくていい
// <form action={submitAction}>
// <input name="name" />
// <SubmitButton>更新</SubmitButton>
// </form>
useFormStatus は react-dom から import します。 react 本体ではないので注意。SSR / Server Components 環境でも同じですが、フォーム要素の子孫である必要があります。useOptimistic ── 楽観的 UI を宣言的に
useOptimistic は「非同期リクエストの応答を待たずに、UI 上では成功したかのように即時反映する」ための専用フックです。失敗時は自動で元の値に戻ります。
"use client";
import { useOptimistic } from "react";
type Props = { postId: string; initialLikes: number };
export function LikeButton({ postId, initialLikes }: Props) {
const [optimisticLikes, addOptimistic] = useOptimistic(
initialLikes,
(state: number, increment: number) => state + increment
);
async function likeAction() {
addOptimistic(1); // UI 即時更新
await fetch(`/api/like/${postId}`, { method: "POST" });
// 失敗時は useOptimistic が自動で元に戻す
}
return (
<form action={likeAction}>
<button type="submit">❤️ {optimisticLikes}</button>
</form>
);
}
use() フック ── Promise と Context を render 内で読む
use() は render 内で Promise / Context を読み取る汎用フックです。他のフックと異なり、条件分岐やループ内で呼び出せるのが最大の特徴です。
Promise の読み取り(Suspense 連動)
import { Suspense, use } from "react";
type Comment = { id: string; body: string };
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise); // Suspend してくれる
return (
<ul>{comments.map((c) => <li key={c.id}>{c.body}</li>)}</ul>
);
}
export function CommentsSection({ postId }: { postId: string }) {
const commentsPromise = fetchComments(postId); // render の外で作っても OK
return (
<Suspense fallback={<p>読み込み中...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}
Context の条件付き読み取り
function Heading({ children }: { children?: React.ReactNode }) {
if (!children) return null; // ← useContext では書けなかった
const theme = use(ThemeContext); // use() なら early return 後でも OK
return <h1 style={{ color: theme.color }}>{children}</h1>;
}
useContext と use(Context) は似て非なるフックです。 use() は条件分岐・ループの中で呼べるため、「このプロパティがある時だけ Context を読む」のような制御が書けます。ただし Server Components で Context を使う場合は依然として制約があるので、フレームワーク(Next.js 等)の要件を確認してください。ref as prop ── forwardRef の廃止
React 19 では関数コンポーネントが普通の ref プロパティを受け取れるようになりました。forwardRef は将来のメジャーバージョンで削除予定です。
import { forwardRef } from "react";
const MyInput = forwardRef<HTMLInputElement, { placeholder: string }>(
function MyInput({ placeholder }, ref) {
return <input ref={ref} placeholder={placeholder} />;
}
);
type Props = {
placeholder: string;
ref?: React.Ref<HTMLInputElement>;
};
export function MyInput({ placeholder, ref }: Props) {
return <input ref={ref} placeholder={placeholder} />;
}
// 使う側は変わらない
// <MyInput ref={inputRef} placeholder="..." />
ref クリーンアップ関数
<input
ref={(el) => {
console.log("mounted", el);
return () => {
console.log("unmounted"); // ref 解除時に呼ばれる
};
}}
/>
ref コールバックで完結できます。<Context> as provider ── Context.Provider の簡略化
const ThemeContext = createContext<"light" | "dark">("light");
// 従来
<ThemeContext.Provider value="dark">
<App />
</ThemeContext.Provider>
// React 19(こちらが推奨)
<ThemeContext value="dark">
<App />
</ThemeContext>
Document Metadata ── コンポーネント内で <title> / <meta> を書く
React 19 は <title> / <meta> / <link> をコンポーネントのどこに書いても自動で <head> に移動させます。react-helmet のような外部ライブラリが不要になりました。
export function BlogPost({ post }: { post: Post }) {
return (
<article>
<title>{post.title} | codingls.com</title>
<meta name="description" content={post.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<link rel="canonical" href={`https://example.com/${post.slug}`} />
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
);
}
Stylesheet precedence ── 読み込み順をコンポーネントで宣言
function Layout() {
return (
<>
<link rel="stylesheet" href="/reset.css" precedence="default" />
<link rel="stylesheet" href="/theme.css" precedence="high" />
<link rel="stylesheet" href="/print.css" precedence="low" />
<main>...</main>
</>
);
}
// 同一 URL を複数コンポーネントで書いても React が一度しか挿入しない
precedence="default" を指定しておけば順序が保証されます。ユーザー定義スタイル(high)を後から注入して上書きする設計も可能です。Resource Preloading APIs ── パフォーマンスを宣言的に上げる
import { preinit, preload, preconnect, prefetchDNS } from "react-dom";
function HomePage() {
// 後続で使うスクリプトを先に初期化
preinit("https://cdn.example.com/analytics.js", { as: "script" });
// フォント・CSS を先行ロード
preload("/fonts/Noto-Sans-JP.woff2", { as: "font", type: "font/woff2", crossOrigin: "anonymous" });
// 別オリジンへの接続を先に確立
preconnect("https://api.example.com");
// DNS 解決だけ先行
prefetchDNS("https://metrics.example.com");
return <div>...</div>;
}
<link rel="preload"> を index.html に書き分けていた部分が、コンポーネント内で宣言できるようになりました。 ルートごとに必要なリソースだけを正確に先読みできるため、LCP / FCP の改善に直結します。パフォーマンス最適化の全体像は Claude Code でフロントエンドパフォーマンス最適化 を参照してください。Server Components と Server Actions(安定化)
React 19 で Server Components と Server Actions が安定扱いになりました。Next.js App Router などのフレームワーク上で、サーバー側で実行するコンポーネントとクライアントから直接呼べるサーバー関数を書けます。
// app/posts/page.tsx(Next.js App Router)
// "use client" なし = Server Component
import { prisma } from "@/lib/prisma";
export default async function PostsPage() {
const posts = await prisma.post.findMany({ orderBy: { createdAt: "desc" } });
return (
<ul>
{posts.map((p) => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
// app/posts/actions.ts
"use server";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
await prisma.post.create({ data: { title } });
revalidatePath("/posts");
}
// app/posts/new/page.tsx
import { createPost } from "../actions";
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<button>作成</button>
</form>
);
}
React 19.2 の目玉 ── <Activity /> と useEffectEvent
<Activity /> コンポーネント
<Activity /> は「UI を隠しつつ state を保持する」ための公式 API です。タブ切替・モーダル・ルーター裏画面などで、「unmount するほどではないが表示は止めたい」ケースを mode="hidden" で表現できます。
import { Activity, useState } from "react";
export function TabbedView() {
const [active, setActive] = useState<"a" | "b">("a");
return (
<>
<nav>
<button onClick={() => setActive("a")}>A</button>
<button onClick={() => setActive("b")}>B</button>
</nav>
<Activity mode={active === "a" ? "visible" : "hidden"}>
<TabA /> {/* 非表示時も state は残る */}
</Activity>
<Activity mode={active === "b" ? "visible" : "hidden"}>
<TabB />
</Activity>
</>
);
}
<Activity /> は副作用(effects)を停止し、レンダリングの優先度を下げます。display:none は DOM は残るが React 側で「実行中」と見なされ続けるため、重い effects が裏でも走ります。Activity なら CPU / バッテリーの消費も抑えられます。useEffectEvent フック
useEffectEvent は「Effect の中で最新の props / state を見つつ、依存配列には入れない」イベント関数を作るためのフックです。これまで古いクロージャに悩まされていたケースを根本解決します。
import { useEffect, useEffectEvent } from "react";
type Props = { roomId: string; theme: "light" | "dark" };
function ChatRoom({ roomId, theme }: Props) {
// theme が変わっても effect は再実行したくない
const onConnected = useEffectEvent(() => {
showNotification("Connected!", theme);
});
useEffect(() => {
const conn = createConnection(roomId);
conn.on("connected", () => onConnected());
conn.connect();
return () => conn.disconnect();
}, [roomId]); // theme は依存に入れなくて OK
return <h1>{roomId}</h1>;
}
Performance Tracks(Chrome DevTools 連携)
React 19.2 は Chrome DevTools の Performance タブに React 専用トラックを追加します。コンポーネント単位のレンダー時間・コミットフェーズ・ブロック原因が可視化され、プロファイラ拡張に頼らずパフォーマンス調査ができます。
React 18 → 19 移行ガイド
npm install --save-exact react@19 react-dom@19 npm install --save-dev @types/react@19 @types/react-dom@19 # Compiler も一緒に npm install --save-dev babel-plugin-react-compiler eslint-plugin-react-compiler
破壊的変更と対処
| 変更点 | 対処 |
|---|---|
propTypes / defaultProps の削除(関数コンポーネント) |
TypeScript の型 or デフォルト引数に置き換える |
ReactDOM.render / unmountComponentAtNode 削除 |
createRoot / root.unmount() へ |
Legacy Context(contextTypes)削除 |
createContext API に統一 |
| String refs 削除 | コールバック ref か useRef |
| エラーハンドリングの変更 | onCaughtError / onUncaughtError を root に設定 |
npx codemod react/19/migration-recipe を実行すると一部の変更を自動適用できます。ただし全部は自動化されないため、必ず npm test / E2E テストを通してから main にマージしてください。E2E の組み込みは Claude Code × テスト完全ガイド 参照。落とし穴と注意点
Server Components で useState は使えない
Server Components はサーバーでしか実行されないため、state を持つフック(useState / useEffect など)は使えません。state が必要な部分は子コンポーネントを "use client" で切り出す設計に分割します。
use() の Promise は render ごとに新しくしない
use(fetchSomething()) のように render 内で毎回 Promise を生成すると無限に再実行されます。親で一度だけ作って props で渡すか、フレームワーク(Next.js の fetch)のキャッシュに寄せます。
Compiler 有効化でプロファイラの結果が変わる
Compiler が自動メモ化を行うため、開発中のプロファイラで手動メモ化を足しても効果が見えない・劣化するケースがあります。本番環境は Compiler を有効にした状態でベンチマークを取るのが鉄則です。
ref as prop と forwardRef の混在
移行期にライブラリが forwardRef のまま残ることがあります。型定義上は混在しても動きますが、段階的削除予定なので社内 UI ライブラリを 19 対応する際は新規コンポーネントから ref as prop で書き、順次リプレースしてください。
useFormStatus は react-dom から import
import 元を間違えて react から取ろうとするビルドエラーが頻発します。react は plat 非依存、react-dom は DOM 依存という使い分けを意識してください。
よくある質問
useEffectEvent に移行して根本解決します。useFormStatus で共通 UI(送信中スピナー等)が書きやすくなります。form action={...})や useActionState と組み合わせて呼んだ場合、アクションが throw するか失敗を返すと React が内部で最新の真値に戻します。手動で addOptimistic した分は、その Action が終了した段階で破棄されます。独立した addOptimistic 呼び出し(Action 外)は自動ロールバックしないため、Action ベースの利用が前提です。use() する場合は Suspense は不要です。React.ForwardRefExoticComponent など)を使っているライブラリは公開 API の破壊的変更になるため、ライブラリ開発者は慎重に。<Activity /> が最適です。逆に「完全に unmount して state を捨てたい」時は従来の条件レンダリング({active && <Comp />})のままで構いません。display:none は CSS レイヤーなので React の state 管理とは別次元の話です。npm install next@15)、② React 19 にアップグレード、③ next.config.ts で experimental.reactCompiler = true、④ Server Actions を使う部分を "use server" に整理、⑤ フォームを useActionState へリファクタ——の順が実害最小です。詳細は Claude Code × Next.js フルスタック開発完全ガイド を参照してください。まとめ
- React 19.2 + React Compiler 1.0 が 2026 年のデフォルト。新規は最初から、既存は Compiler 先行導入が安全
- Actions + useActionState + useFormStatus + useOptimistic: フォームのボイラープレートが 40〜60% 減り、楽観的 UI も宣言的に
- use() フックは条件分岐・ループ内で Promise / Context を読める。Suspense との連携が自然に
- ref as propで forwardRef から解放。ref クリーンアップ関数で ref ベースのリソース管理も宣言的に
- Document Metadata / Stylesheet precedence / Resource Preloading: 外部ライブラリに頼らずコンポーネント内で head 制御が完結
- Server Components / Server Actions 安定化: Next.js App Router と合わせて最小ボイラープレートの全スタック構成
- React 19.2: <Activity /> で UI 非表示時も state 保持、useEffectEvent で stale closure 根絶、Performance Tracks で DevTools 統合
- 移行は codemod → テスト → 手作業リファクタの 3 ステップ。Compiler の ESLint 警告は純粋性ルールへの違反シグナルなので、先に潰してから Compiler を有効化すると事故が減る
Next.js でのフルスタック設計は Claude Code × Next.js フルスタック開発完全ガイド、状態管理の全体像は Claude Code で React 状態管理を設計する実践ガイド、フロントエンドパフォーマンス最適化は Claude Code でフロントエンドパフォーマンス最適化、Supabase との連携は Claude Code × Supabase フルスタック開発完全ガイド もあわせてご覧ください。
