React 19正式リリース - Actions、use()、新しいフックの全貌

2025.12.02

React 19.0 TypeScript 5.5+
公式ドキュメント

React 19が正式リリースされ、Server Actions、新しいフック、フォーム処理の大幅な改善など、多くの新機能が追加されました。本記事では、React 19の主要な新機能を実践的なコード例とともに解説します。

React 19の主な新機能

概要

flowchart TB
 subgraph React19["React 19"]
 subgraph Hooks["新しいフック"]
 H1["use() - Promise/Contextの読み取り"]
 H2["useActionState() - フォームアクション状態"]
 H3["useFormStatus() - フォーム送信状態"]
 H4["useOptimistic() - 楽観的更新"]
 end

 subgraph Actions["Actions"]
 A1["Server Actions - サーバーサイド関数"]
 A2["Client Actions - クライアント非同期処理"]
 A3["フォーム統合 - form action"]
 end

 subgraph Others["その他の改善"]
 O1["ref as prop - forwardRef不要"]
 O2["Document Metadata - title等の直接記述"]
 O3["Stylesheet管理 - precedenceによる順序制御"]
 O4["Resource Preloading - prefetch/preload API"]
 end
 end

use() フック

Promiseの読み取り

use()は、レンダリング中にPromiseやContextを読み取るための新しいフックです。

// use() - Promiseの読み取り
import { use, Suspense } from 'react';

// データフェッチ関数
async function fetchUser(id: string): Promise<User> {
 const response = await fetch(`/api/users/${id}`);
 return response.json();
}

// コンポーネント
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
 // Suspenseと組み合わせて使用
 const user = use(userPromise);

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

// 親コンポーネント
function UserPage({ userId }: { userId: string }) {
 // Promiseをpropsとして渡す
 const userPromise = fetchUser(userId);

 return (
 <Suspense fallback={<div>Loading user...</div>}>
 <UserProfile userPromise={userPromise} />
 </Suspense>
 );
}

// 条件付きでuse()を呼び出せる(他のフックとの違い)
function ConditionalData({ shouldFetch, dataPromise }: {
 shouldFetch: boolean;
 dataPromise: Promise<Data>;
}) {
 if (!shouldFetch) {
 return <div>No data needed</div>;
 }

 // 条件分岐の後でも使用可能
 const data = use(dataPromise);
 return <div>{data.value}</div>;
}

Contextの読み取り

// use() - Contextの読み取り
import { use, createContext } from 'react';

const ThemeContext = createContext<'light' | 'dark'>('light');

function ThemedButton() {
 // useContext()の代わりにuse()を使用可能
 const theme = use(ThemeContext);

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

// 条件付きでContextを読み取り
function ConditionalTheme({ useTheme }: { useTheme: boolean }) {
 if (!useTheme) {
 return <button>Default Button</button>;
 }

 // 条件分岐後でも使用可能
 const theme = use(ThemeContext);
 return <button className={`theme-${theme}`}>Themed Button</button>;
}

Actions

Server Actions

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

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

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

 // バリデーション
 if (!title || title.length < 3) {
 return { error: 'タイトルは3文字以上必要です' };
 }

 // データベースに保存
 const post = await db.post.create({
 data: { title, content },
 });

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

 // リダイレクト
 redirect(`/posts/${post.id}`);
}

// 更新アクション
export async function updatePost(id: string, formData: FormData) {
 const title = formData.get('title') as string;
 const content = formData.get('content') as string;

 await db.post.update({
 where: { id },
 data: { title, content },
 });

 revalidatePath(`/posts/${id}`);
 return { success: true };
}

// 削除アクション
export async function deletePost(id: string) {
 await db.post.delete({ where: { id } });
 revalidatePath('/posts');
 redirect('/posts');
}

フォームとの統合

// app/posts/new/page.tsx
'use client';

import { useActionState } from 'react';
import { createPost } from '../actions';

export default function NewPostPage() {
 // useActionState - フォームアクションの状態管理
 const [state, formAction, isPending] = useActionState(
 createPost,
 { error: null }
 );

 return (
 <form action={formAction}>
 <div>
 <label htmlFor="title">タイトル</label>
 <input
 id="title"
 name="title"
 required
 disabled={isPending}
 />
 </div>

 <div>
 <label htmlFor="content">内容</label>
 <textarea
 id="content"
 name="content"
 disabled={isPending}
 />
 </div>

 {state?.error && (
 <p className="text-red-500">{state.error}</p>
 )}

 <button type="submit" disabled={isPending}>
 {isPending ? '投稿中...' : '投稿する'}
 </button>
 </form>
 );
}

useFormStatus

// フォーム送信状態の取得
import { useFormStatus } from 'react-dom';

function SubmitButton() {
 // 親の<form>の送信状態を取得
 const { pending, data, method, action } = useFormStatus();

 return (
 <button type="submit" disabled={pending}>
 {pending ? (
 <>
 <Spinner />
 送信中...
 </>
 ) : (
 '送信'
 )}
 </button>
 );
}

// フォームで使用
function ContactForm() {
 async function submitForm(formData: FormData) {
 'use server';
 // 送信処理
 }

 return (
 <form action={submitForm}>
 <input name="email" type="email" required />
 <textarea name="message" required />
 {/* SubmitButtonは親formの状態を自動で取得 */}
 <SubmitButton />
 </form>
 );
}

useOptimistic - 楽観的更新

// 楽観的更新の実装
import { useOptimistic, startTransition } from 'react';

interface Message {
 id: string;
 text: string;
 sending?: boolean;
}

function ChatMessages({ messages }: { messages: Message[] }) {
 // 楽観的な状態管理
 const [optimisticMessages, addOptimisticMessage] = useOptimistic(
 messages,
 (state, newMessage: Message) => [
 ...state,
 { ...newMessage, sending: true },
 ]
 );

 async function sendMessage(formData: FormData) {
 const text = formData.get('message') as string;
 const tempId = crypto.randomUUID();

 // 楽観的に追加
 startTransition(() => {
 addOptimisticMessage({
 id: tempId,
 text,
 sending: true,
 });
 });

 // サーバーに送信
 await fetch('/api/messages', {
 method: 'POST',
 body: JSON.stringify({ text }),
 });
 }

 return (
 <div>
 <ul>
 {optimisticMessages.map((message) => (
 <li
 key={message.id}
 className={message.sending ? 'opacity-50' : ''}
 >
 {message.text}
 {message.sending && <span> (送信中...)}
 </li>
 ))}
 </ul>

 <form action={sendMessage}>
 <input name="message" required />
 <button type="submit">送信</button>
 </form>
 </div>
 );
}

いいねボタンの例

// 楽観的ないいねボタン
function LikeButton({ postId, initialLiked, initialCount }: {
 postId: string;
 initialLiked: boolean;
 initialCount: number;
}) {
 const [{ liked, count }, setOptimistic] = useOptimistic(
 { liked: initialLiked, count: initialCount },
 (state, newLiked: boolean) => ({
 liked: newLiked,
 count: state.count + (newLiked ? 1 : -1),
 })
 );

 async function toggleLike() {
 const newLiked = !liked;

 // 楽観的に更新
 startTransition(() => {
 setOptimistic(newLiked);
 });

 // サーバーに送信
 await fetch(`/api/posts/${postId}/like`, {
 method: newLiked ? 'POST' : 'DELETE',
 });
 }

 return (
 <button onClick={toggleLike}>
 {liked ? '❤️' : '🤍'} {count}
 </button>
 );
}

ref as prop

React 19では、forwardRefなしでrefをpropsとして受け取れるようになりました。

// React 18以前: forwardRefが必要
const InputOld = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
 return <input ref={ref} {...props} />;
});

// React 19: refは通常のpropとして渡せる
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
 return <input ref={ref} {...props} />;
}

// 使用例
function Form() {
 const inputRef = useRef<HTMLInputElement>(null);

 return (
 <form>
 <Input ref={inputRef} placeholder="Enter text" />
 <button
 type="button"
 onClick={() => inputRef.current?.focus()}
 >
 Focus
 </button>
 </form>
 );
}

Document Metadata

コンポーネント内でメタデータを直接記述できるようになりました。

// メタデータの直接記述
function BlogPost({ post }: { post: Post }) {
 return (
 <article>
 {/* ドキュメントのheadに自動的にホイストされる */}
 <title>{post.title} - My Blog</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://myblog.com/posts/${post.slug}`} />

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

// 複数ページでの使用
function ProductPage({ product }: { product: Product }) {
 return (
 <div>
 <title>{product.name} | My Store</title>
 <meta name="description" content={product.description} />

 {/* 構造化データ */}
 <script type="application/ld+json">
 {JSON.stringify({
 '@context': 'https://schema.org',
 '@type': 'Product',
 name: product.name,
 description: product.description,
 price: product.price,
 })}
 </script>

 <h1>{product.name}</h1>
 <p>{product.description}</p>
 </div>
 );
}

Stylesheet管理

// スタイルシートの優先度管理
function ComponentWithStyles() {
 return (
 <>
 {/* precedenceで読み込み順序を制御 */}
 <link
 rel="stylesheet"
 href="/styles/base.css"
 precedence="default"
 />
 <link
 rel="stylesheet"
 href="/styles/components.css"
 precedence="default"
 />
 <link
 rel="stylesheet"
 href="/styles/utilities.css"
 precedence="high"
 />

 <div className="styled-component">
 Content
 </div>
 </>
 );
}

// 動的スタイルシート
function ThemeSwitcher({ theme }: { theme: 'light' | 'dark' }) {
 return (
 <>
 <link
 rel="stylesheet"
 href={`/themes/${theme}.css`}
 precedence="high"
 />
 <div>Themed content</div>
 </>
 );
}

Resource Preloading

// リソースの事前読み込みAPI
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';

function ResourceHints() {
 // DNS事前解決
 prefetchDNS('https://api.example.com');

 // 事前接続
 preconnect('https://cdn.example.com');

 // リソースの事前読み込み
 preload('/fonts/custom.woff2', {
 as: 'font',
 type: 'font/woff2',
 crossOrigin: 'anonymous',
 });

 // スクリプトの事前初期化
 preinit('/scripts/analytics.js', {
 as: 'script',
 });

 return <div>Content</div>;
}

// 画像の事前読み込み
function ImageGallery({ images }: { images: string[] }) {
 // 次の画像を事前読み込み
 useEffect(() => {
 images.slice(1, 4).forEach((src) => {
 preload(src, { as: 'image' });
 });
 }, [images]);

 return (
 <div>
 {images.map((src) => (
 <img key={src} src={src} alt="" />
 ))}
 </div>
 );
}

React Compiler (実験的)

// React Compilerによる自動メモ化

// Before: 手動でuseMemo/useCallbackが必要
function ProductListOld({ products, onSelect }: Props) {
 const sortedProducts = useMemo(
 () => [...products].sort((a, b) => a.price - b.price),
 [products]
 );

 const handleSelect = useCallback(
 (id: string) => onSelect(id),
 [onSelect]
 );

 return (
 <ul>
 {sortedProducts.map((product) => (
 <ProductItem
 key={product.id}
 product={product}
 onSelect={handleSelect}
 />
 ))}
 </ul>
 );
}

// After: React Compilerが自動でメモ化
function ProductList({ products, onSelect }: Props) {
 // 手動のメモ化不要 - コンパイラが最適化
 const sortedProducts = [...products].sort((a, b) => a.price - b.price);

 return (
 <ul>
 {sortedProducts.map((product) => (
 <ProductItem
 key={product.id}
 product={product}
 onSelect={(id) => onSelect(id)}
 />
 ))}
 </ul>
 );
}

// babel.config.js でReact Compilerを有効化
module.exports = {
 plugins: [
 ['babel-plugin-react-compiler', {
 // オプション
 }],
 ],
};

エラーハンドリングの改善

// 改善されたエラー表示

// hydrationエラーの詳細表示
// React 19では、差分が具体的に表示される

// エラーバウンダリの改善
class ErrorBoundary extends React.Component<
 { children: React.ReactNode; fallback: React.ReactNode },
 { hasError: boolean; error: Error | null }
> {
 state = { hasError: false, error: null };

 static getDerivedStateFromError(error: Error) {
 return { hasError: true, error };
 }

 componentDidCatch(error: Error, info: React.ErrorInfo) {
 // React 19: より詳細なスタックトレース
 console.error('Error:', error);
 console.error('Component Stack:', info.componentStack);
 }

 render() {
 if (this.state.hasError) {
 return this.props.fallback;
 }
 return this.props.children;
 }
}

// 使用例
function App() {
 return (
 <ErrorBoundary fallback={<ErrorPage />}>
 <MainContent />
 </ErrorBoundary>
 );
}

マイグレーションガイド

React 18 → 19 への移行

# パッケージの更新
npm install react@19 react-dom@19

# TypeScript型定義
npm install -D @types/react@19 @types/react-dom@19
// 主な変更点

// 1. forwardRef → 通常のprop
// Before
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => (
 <input ref={ref} {...props} />
));

// After
function Input({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }) {
 return <input ref={ref} {...props} />;
}

// 2. useContext → use (オプション)
// Before
const theme = useContext(ThemeContext);

// After (条件付きで使用する場合に便利)
const theme = use(ThemeContext);

// 3. 非推奨APIの削除
// - defaultProps (関数コンポーネント)
// - propTypes
// - createFactory
// - render (react-dom)

// defaultPropsの代替
// Before
function Button({ size = 'medium' }) { ... }
Button.defaultProps = { size: 'medium' };

// After (デフォルトパラメータを使用)
function Button({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) {
 // ...
}

まとめ

React 19は、開発者体験を大幅に向上させる多くの新機能を提供しています。

主要な新機能

機能用途
use()Promise/Contextの読み取り
useActionStateフォームアクションの状態管理
useFormStatus送信状態の取得
useOptimistic楽観的更新
Server Actionsサーバーサイド処理
ref as propforwardRef不要

移行の推奨

  • 新規プロジェクト: React 19を採用
  • 既存プロジェクト: 段階的に移行
  • Server Actions: Next.js 14+と組み合わせ

React 19により、より直感的で高速なReactアプリケーション開発が可能になります。

参考リンク

参考リソース

← 一覧に戻る
PR
PR
PR
PR