10月17日、jsdev.spaceが「React 19.3: Three Changes Worth Your Attention」と題した記事を公開した。この記事では、React 19.3で安定版となった3つの機能(<ViewTransition>、Fragment Refs、browser() API)について詳しく解説されている。
React 19.3は2026年9月9日にリリースされた。React 19.0(2024年12月)でServer Actions・use()・ref-as-propなどの大型機能が投入されて以降、19系はマイナーリリースを重ねながら実験的APIの安定化を進めてきた。今回安定化した3機能に共通するテーマは明確だ——これまでラッパー要素・追加state・サードパーティライブラリで迂回してきた実装パターンに、Reactネイティブな解法を提供することである。
1. <ViewTransition>:Reactのスケジューリングに統合されたアニメーション
フロントエンドでアニメーションを実装する際の難点の一つは「Reactのレンダリングとタイミングを合わせること」だ。CSSアニメーションやMotionのようなライブラリでは、コンポーネントの出現・消滅タイミングとアニメーションの協調が複雑になりがちだった。
React 19.3の<ViewTransition>は、ブラウザのView Transition APIとReactのスケジューリングシステムを統合することでこの問題に対処する。View Transition API自体はChrome・Edge・Safariに実装済みで、Firefoxも対応済みとなり主要ブラウザで広く利用可能な状況だ。
基本的な使い方
パネルの表示・非表示を切り替える例:
import {
useState,
startTransition,
ViewTransition,
} from 'react';
export default function DetailsPanel() {
const [expanded, setExpanded] = useState(false);
function handlePanelToggle() {
startTransition(() => {
setExpanded(previous => !previous);
});
}
return (
<section>
<button
type="button"
onClick={handlePanelToggle}
aria-expanded={expanded}
>
{expanded ? 'Hide details' : 'Show details'}
</button>
{expanded && (
<ViewTransition>
<article className="details-panel">
<h2>Project overview</h2>
<p>Here you can find the latest project updates.</p>
</article>
</ViewTransition>
)}
</section>
);
}
startTransition()が必須である点に注意が必要だ。startTransition()はReactに「この更新は緊急ではなく、遷移として扱ってよい」と伝えるAPIで、これを使わない通常の状態更新ではView Transitionはトリガーされない。
アニメーションの種類とカスタマイズ
Reactがサポートするトランジションは4タイプだ。
| タイプ | タイミング |
|---|---|
| enter | ViewTransitionが出現する |
| exit | ViewTransitionが消える |
| update | 内容やスタイルが変化する |
| share | 名前付きViewTransitionが別の場所へ移動する |
デフォルトはクロスフェードだが、CSSでカスタマイズできる。以下はパネルをスライドインさせる例:
<ViewTransition enter="panel-enter" exit="panel-exit">
<article className="details-panel">
<h2>Account settings</h2>
<p>Manage your personal preferences.</p>
</article>
</ViewTransition>
@keyframes panel-appear {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
::view-transition-new(.panel-enter) {
animation: 220ms ease-out both panel-appear;
}
@media (prefers-reduced-motion: reduce) {
::view-transition-new(.panel-enter) {
animation: none;
}
}
prefers-reduced-motionへの対応は自分で書く必要があるが、CSSレイヤーで完結できる点は扱いやすい。
SuspenseとViewTransitionの組み合わせ
<ViewTransition>はSuspenseとも組み合わせられる。lazy loadedなコンポーネントがフォールバックから実コンテンツへ切り替わる際にアニメーションを付与できる。
import { Suspense, lazy, ViewTransition } from 'react';
const AnalyticsDashboard = lazy(
() => import('./AnalyticsDashboard.jsx')
);
export default function DashboardSection() {
return (
<ViewTransition>
<Suspense fallback={<p>Preparing your dashboard...</p>}>
<AnalyticsDashboard />
</Suspense>
</ViewTransition>
);
}
記事では「ローディングインジケーターはすぐ表示すべきで、フォールバック自体をアニメーションさせるよりも、コンテンツが出現する瞬間をアニメーションさせる方が体験として適切」と指摘している。
2. Fragment Refs:ラッパー不要でDOM要素グループを操作
<Fragment>(<>...</>)は複数の要素をDOMに余分なノードを追加せずに返せる仕組みだ。ただし従来は、Fragment内の要素群をまとめてrefで参照したい場合、<div>などのラッパーを追加するしかなかった。このラッパーがCSSのflexやgridレイアウト、またはARIAを活用したセマンティクスに悪影響を与えるケースがあった。
React 19.3では、明示的な<Fragment>に直接refを渡せるようになった。ref.currentは通常のHTMLElementではなくFragmentInstanceという専用オブジェクトになる。FragmentInstanceが持つメソッドは以下のとおりだ:
| メソッド | 用途 |
|---|---|
focus() |
最初のフォーカス可能な子要素にフォーカス |
focusLast() |
最後のフォーカス可能な子要素にフォーカス |
blur() |
フォーカスを外す |
addEventListener() / removeEventListener() |
第一レベルの子DOMにイベントリスナーを付与 |
observeUsing() / unobserveUsing() |
IntersectionObserver / ResizeObserverと接続 |
getClientRects() |
レイアウト矩形を取得 |
scrollIntoView() |
Fragmentのコンテンツをスクロールして表示 |
import { Fragment, useEffect, useRef } from 'react';
export default function QuickActions() {
const actionsGroup = useRef(null);
useEffect(() => {
// FragmentInstanceのfocus()は最初のフォーカス可能な子要素にフォーカスする
actionsGroup.current?.focus();
}, []);
return (
<Fragment ref={actionsGroup}>
<button type="button">Create project</button>
<button type="button">Import files</button>
<button type="button">Open settings</button>
</Fragment>
);
}
コンポーネントライブラリのナビゲーショングループやカスタムUIプリミティブを実装する場面で、ラッパー要素なしにDOM操作できる実用的な選択肢となる。
3. browser():SSRとブラウザAPIの境界を宣言的に示す
localStorageやタイムゾーン情報など、ブラウザ固有のAPIに依存するコンポーネントはSSR時にエラーを起こす。従来はuseEffect + マウントフラグ用stateで「マウント後に読み込む」パターンが一般的だったが、ボイラープレートが多く、実装漏れによるハイドレーションエラーも起きやすかった。
React 19.3ではreact-domのbrowser()を使う方法が追加された。
import { Suspense, use } from 'react';
import { browser } from 'react-dom';
function StoredThemePreference() {
use(browser('Theme preference requires localStorage.'));
const selectedTheme = localStorage.getItem('theme') ?? 'system';
return <p>Preferred theme: {selectedTheme}</p>;
}
export default function PreferencesPage() {
return (
<section>
<h2>Appearance</h2>
<Suspense fallback={<p>Loading preferences...</p>}>
<StoredThemePreference />
</Suspense>
</section>
);
}
サーバー上ではuse(browser())がコンポーネントをサスペンドし、最寄りのSuspenseフォールバックが表示される。ブラウザ上ではサスペンドしない。 マウントフラグ用のstateが不要になり、環境の分岐を宣言的に書ける。
利用にあたっての制約は2点ある:
- Suspenseバウンダリの内側で使うこと
- Server Componentでは使えない。Next.jsなど
'use client'ディレクティブが必要なフレームワークでは明示的に指定する必要がある
また、browser()はレンダリング環境の境界を示すものだ。ストレージへのアクセス失敗やパーミッションエラーといったランタイムエラーのハンドリングは引き続き自分で実装する必要がある。
3つの変更はReactの基本的な書き方を根本から変えるものではない。ただ、アニメーション・DOM操作・SSR境界という異なる文脈のそれぞれで「workaroundが不要になった」点は、既存コードベースを持つチームにとって地道に効いてくる変化だ。特にSSRを前提としたNext.jsやRemixを使っているプロジェクトでは、browser()と<ViewTransition>の2つから試してみる価値がある。
詳細はReact 19.3: Three Changes Worth Your Attentionを参照していただきたい。