🚀

【日本語訳】React 19 安定版リリースノート

2024/12/10に公開

はじめに

2024年12月5日、React 19が安定版としてリリースされましたね🚀

本記事では公式リリースノートの内容を、DeepL翻訳を活用して日本語に翻訳しました。

より詳細な情報は、ぜひ公式リリースノートをご確認ください。
翻訳元📕は、こちらです。

React 19が正式にリリースされました

  • npmで利用可能になりました
  • 4月のRC発表から、suspended treesのプリウォーミングとReact DOM static APIが追加されました。

React 19の新機能

Actions

Reactアプリの一般的なユースケースは、データ変異を実行し、それに応答して状態を更新することです。

例えば、ユーザーが名前を変更するためにフォームを送信すると、APIリクエストを行い、レスポンスを処理します。

以前は、保留状態、エラー、楽観的更新、逐次リクエストを手動で処理する必要がありました。

例えば、保留状態やエラー状態をuseStateで処理することができました:

// 以前のActions
function UpdateName({}) {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);

  const handleSubmit = async () => {
    setIsPending(true);
    const error = await updateName(name);
    setIsPending(false);
    if (error) {
      setError(error);
      return;
    } 
    redirect("/path");
  };

  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

React 19では、トランジションで非同期関数を使用して、保留中のステート、エラー、フォーム、楽観的な更新を自動的に処理するためのサポートを追加します。

例えば、useTransitionを使えば、ペンディング状態を処理することができます。

// Actionsからpending状態を利用する
function UpdateName({}) {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  const handleSubmit = () => {
    startTransition(async () => {
      const error = await updateName(name);
      if (error) {
        setError(error);
        return;
      } 
      redirect("/path");
    })
  };

  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

非同期トランジションは直ちに isPending 状態を true に設定し、非同期リクエストを行い、トランジション後に isPending を false に切り替えます。

これにより、データが変更されている間、現在の UI の応答性と対話性を維持することができます。

Actionsの上に構築されたReact 19は、楽観的な更新を管理するuseOptimisticと、Actionsの一般的なケースを処理する新しいフックReact.useActionStateを導入しています。

react-domでは、フォームを自動的に管理するための<form>アクションと、フォームでのアクションの一般的なケースをサポートするuseFormStatusを追加しています。

React 19では、上記の例は次のように単純化できます:

// <form>アクションとuseActionStateの使用
function ChangeName({ name, setName }) {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await updateName(formData.get("name"));
      if (error) {
        return error;
      }
      redirect("/path");
      return null;
    },
    null,
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}

次のセクションでは、React 19の新しいActionの各機能を説明します。

新しいhook: useActionState

アクションでよくあるケースを簡単にするために、useActionStateという新しいフックが追加されました:

const [error, submitAction, isPending] = useActionState(
  async (previousState, newName) => {
    const error = await updateName(newName);
    if (error) {
      // アクションの結果は何でも返すことができます。
      // ここでは、エラーだけを返します。
      return error;
    }

    // 成功の処理
    return null;
  },
  null,
);

useActionStateは関数(アクション)を受け取り、呼び出すためのラップされたアクションを返す。これは、Actionが合成されるためです。

ラップされたActionが呼び出されると、useActionStateはActionの最後の結果をdataとして返し、Actionの保留状態をpendingとして返します。

React DOM: <form> Actions

ActionsはReact 19のreact-domの新しいフォーム機能とも統合されています。

<form><input><button> 要素の action および formAction propsに関数を渡すことで、Actions を使って自動的にフォームを送信できるようになりました:

<form action={actionFunction}>

<form> アクションが成功すると、React は制御されていないコンポーネントのフォームを自動的にリセットします。

<form> を手動でリセットする必要がある場合は、新しい requestFormReset React DOM API を呼び出すことができます。

React DOM: 新しいhook: useFormStatus

デザインシステムにおいて、コンポーネントに小道具を掘り下げることなく、<form> 内の情報にアクセスする必要があるデザインコンポーネントを書くことはよくあります。

これは Context を使って行うことができますが、よくあるケースをより簡単にするために、新しいフック useFormStatus を追加しました:

import {useFormStatus} from 'react-dom';

function DesignButton() {
  const {pending} = useFormStatus();
  return <button type="submit" disabled={pending} />
}

useFormStatus は親<form>のステータスを、あたかもそのフォームが Context プロバイダであるかのように読み込みます。

新しいhook: useOptimistic

データ変異を実行するときのもう1つの一般的なUIパターンは、非同期リクエストの進行中に最終的な状態を楽観的に表示することです。

React 19では、これを簡単にするためにuseOptimisticという新しいフックが追加されました:

function ChangeName({currentName, onUpdateName}) {
  const [optimisticName, setOptimisticName] = useOptimistic(currentName);

  const submitAction = async formData => {
    const newName = formData.get("name");
    setOptimisticName(newName);
    const updatedName = await updateName(newName);
    onUpdateName(updatedName);
  };

  return (
    <form action={submitAction}>
      <p>Your name is: {optimisticName}</p>
      <p>
        <label>Change Name:</label>
        <input
          type="text"
          name="name"
          disabled={currentName !== optimisticName}
        />
      </p>
    </form>
  );
}

useOptimisticフックは、updateNameリクエストが進行している間、直ちにoptimisticNameをレンダリングします。更新が終了するかエラーが発生すると、React は自動的に currentName の値に切り替えます。

新しいAPI: use

React 19では、renderでリソースを読み込むための新しいAPI、useを導入しています。

例えば、useを使ってプロミスを読み込むと、Reactはプロミスが解決するまでSuspendします:

import {use} from 'react';

function Comments({commentsPromise}) {
  // useはプロミスが解決するまで中断します。
  const comments = use(commentsPromise);
  return comments.map(comment => <p key={comment.id}>{comment}</p>);
}

function Page({commentsPromise}) {
  // コメント欄で`use`がサスペンスされた場合、
  // このサスペンス・バウンダリーが表示されます。
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  )
}

また、useでコンテキストを読むこともでき、早期復帰後など条件付きでコンテキストを読むことができます:

import {use} from 'react';
import ThemeContext from './ThemeContext'

function Heading({children}) {
  if (children == null) {
    return null;
  }
  
  // これはuseContextでは早期リターンのため機能しません。
  const theme = use(ThemeContext);
  return (
    <h1 style={{color: theme.color}}>
      {children}
    </h1>
  );
}

use APIは、フックと同様、レンダー内でのみ呼び出すことができます。フックとは異なり、useは条件付きで呼び出すことができます。

将来的には、useを使用してrenderでリソースを消費する多くの方法をサポートする予定だそうです。

新しいReact DOM Static APIs

静的サイト生成のためにreact-dom/staticに2つの新しいAPIが追加されました:

  • prerender
  • prerenderToNodeStream

これらの新しいAPIは、静的HTML生成のためにデータのロードを待つことで、renderToStringを改良されています。

これらは、Node.js StreamsやWeb Streamsのようなストリーミング環境で動作するように設計されています。

たとえば、Web Stream 環境では、prerender を使用して React ツリーを静的 HTML にプリレンダリングできます:

import { prerender } from 'react-dom/static';

async function handler(request) {
  const {prelude} = await prerender(<App />, {
    bootstrapScripts: ['/main.js']
  });
  return new Response(prelude, {
    headers: { 'content-type': 'text/html' },
  });
}

プリレンダーAPIは、静的なHTMLストリームを返す前に、すべてのデータがロードされるのを待ちます。

ストリームは文字列に変換することも、ストリーミング応答で送信することもできます。

既存の React DOM サーバー・レンダリング API でサポートされている、コンテンツの読み込み時のストリーミングはサポートされていません。

React Server Components

Server Components

Server Componentsは、クライアントアプリケーションやSSRサーバーとは別の環境で、バンドルする前のコンポーネントを先にレンダリングできる新しいオプションです。

この独立した環境が、React Server Componentsの「サーバー」です。

Server Componentsは、CIサーバー上でビルド時に一度だけ実行することも、Webサーバーを使ってリクエストごとに実行することもできます。

React 19には、Canaryチャネルから含まれるReact Server Componentsのすべての機能が含まれています。

これは、Server Componentsとともに出荷されるライブラリが、フルスタックReactアーキテクチャをサポートするフレームワークで使用するために、react-serverエクスポート条件を持つピア依存関係としてReact 19をターゲットにできるようになったことを意味します。

Server Actions

サーバーアクションは、クライアントコンポーネントがサーバー上で実行される非同期関数を呼び出すことを可能にします。

サーバーアクションが"use server"ディレクティブで定義されると、フレームワークは自動的にサーバー関数への参照を作成し、その参照をクライアントコンポーネントに渡します。

その関数がクライアントで呼び出されると、Reactはその関数を実行するリクエストをサーバーに送り、結果を返します。

React 19の改良点

propsとしてのref

React 19から、関数コンポーネントのpropとしてrefにアクセスできるようになりました:

function MyInput({placeholder, ref}) {
  return <input placeholder={placeholder} ref={ref} />
}

//...
<MyInput ref={ref} />

新しい関数コンポーネントはforwardRefを必要としなくなり、新しいref propを使用するようにコンポーネントを自動的に更新するcodemodを公開される予定です。

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

ハイドレーションエラーの差分

また、react-domにおけるハイドレーションエラーのエラーレポートも改善しました。

例えば、ミスマッチに関する情報を一切持たずにDEVで複数のエラーをログに記録する代わりに:
CleanShot 2024-12-10 at 16.31.06@2x.png

現在では、ミスマッチの差分を1つのメッセージとして記録されています:
CleanShot 2024-12-10 at 16.31.36@2x.png

プロバイダーとしての<Context>

React 19では、<Context.Provider>の代わりに<Context>をプロバイダとしてレンダリングできます:

const ThemeContext = createContext('');

function App({children}) {
  return (
    <ThemeContext value="dark">
      {children}
    </ThemeContext>
  );  
}

新しい Context プロバイダは <Context> を使うことができ、既存のプロバイダを変換するための codemod を公開される予定です。

将来のバージョンでは、<Context.Provider> は廃止される予定です。

参照用クリーンアップ関数

refコールバックからクリーンアップ関数を返せるようになりました:

<input
  ref={(ref) => {
    // refの作成

    // NEW: 要素が DOM から削除されたときに ref をリセットするクリーンアップ関数を返します。
    return () => {
      // refのクリーンアップ
    };
  }}
/>

コンポーネントがアンマウントされると、Reactはrefコールバックから返されたクリーンアップ関数を呼び出します。

これは、DOM参照、クラスコンポーネントへの参照、useImperativeHandleで動作します。

refクリーンアップ関数の導入により、refコールバックからそれ以外のものを返すと、TypeScriptによって拒否されるようになりました。

この問題を解決するには、暗黙的なリターンを使わないようにするのが一般的のようです:

- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />

元のコードはHTMLDivElementのインスタンスを返していたので、TypeScriptはこれがクリーンアップ関数なのか、クリーンアップ関数を返したくないのかがわかりませんでした。

このパターンは、no-implicit-ref-callback-returnでコード改造できます。

useDeferredValue 初期値

useDeferredValueinitialValueオプションを追加しました:

function Search({deferredValue}) {
  // 初期レンダリング時の値は''です。
  // その後、延期された値で再レンダリングがスケジュールされます。
  const value = useDeferredValue(deferredValue, '');
  
  return (
    <Results query={value} />
  );
}

initialValueが提供された場合、useDeferredValueはコンポーネントの最初のレンダリングのための値としてそれを返し、返されたdeferredValueでバックグラウンドでの再レンダリングをスケジュールします。

ドキュメント・メタデータのサポート

HTMLでは、<title><link><meta>などのドキュメント・メタデータ・タグは、ドキュメントの<head>セクションに配置するために予約されています。

Reactでは、アプリに適したメタデータを決定するコンポーネントが、<head>をレンダリングする場所から非常に離れていたり、Reactが<head>をまったくレンダリングしなかったりします。

以前は、これらの要素はエフェクトの中に手動で挿入するか、react-helmetのようなライブラリによって挿入する必要があり、Reactアプリケーションをサーバーレンダリングする際には慎重な取り扱いが必要でした。

React 19では、コンポーネント内のドキュメント・メタデータ・タグをネイティブにレンダリングするためのサポートを追加します:

function BlogPost({post}) {
  return (
    <article>
      <h1>{post.title}</h1>
      <title>{post.title}</title>
      <meta name="author" content="Josh" />
      <link rel="author" href="https://twitter.com/joshcstory/" />
      <meta name="keywords" content={post.keywords} />
      <p>
        Eee equals em-see-squared...
      </p>
    </article>
  );
}

React がこのコンポーネントをレンダリングするとき、<title> <link><meta> タグが表示され、ドキュメントの <head> セクションに自動的に格納されます。

これらのメタデータタグをネイティブでサポートすることで、クライアント専用アプリ、ストリーミングSSR、サーバーコンポーネントで確実に動作するようになります。

スタイルシートのサポート

スタイルシートは、外部リンク (<link rel="stylesheet" href="...">) とインライン (<style>...</style>) の両方で、スタイルの優先順位の規則により、DOM 内での配置に注意が必要です。

コンポーネント内で合成可能なスタイルシート機能を構築するのは困難であるため、ユーザーは多くの場合、スタイルに依存する可能性のあるコンポーネントから離れた場所ですべてのスタイルをロードするか、この複雑さをカプセル化するスタイル ライブラリを使用することになります。

React 19では、この複雑さに対処し、スタイルシートのビルトイン・サポートを使用して、クライアント上の並行レンダリングとサーバー上のストリーミング・レンダリングへの統合をさらに深くしています。

Reactにスタイルシートの優先順位を伝えると、DOM内のスタイルシートの挿入順序を管理し、スタイルルールに依存するコンテンツを表示する前にスタイルシート(外部の場合)が読み込まれるようにします。

function ComponentOne() {
  return (
    <Suspense fallback="loading...">
      <link rel="stylesheet" href="foo" precedence="default" />
      <link rel="stylesheet" href="bar" precedence="high" />
      <article class="foo-class bar-class">
        {...}
      </article>
    </Suspense>
  )
}

function ComponentTwo() {
  return (
    <div>
      <p>{...}</p>
      <link rel="stylesheet" href="baz" precedence="default" />  <-- will be inserted between foo & bar
    </div>
  )
}

サーバーサイドレンダリングの間、React はスタイルシートを <head> に含めます。これは、スタイルシートが読み込まれるまでブラウザが描画しないようにするためです。

すでにストリーミングを開始した後でスタイルシートが発見された場合、React はそのスタイルシートに依存する Suspenseバウンダリのコンテンツを公開する前に、スタイルシートがクライアントの <head> に挿入されるようにします。

クライアントサイドレンダリングの間、Reactはレンダリングをコミットする前に、新しくレンダリングされたスタイルシートがロードされるのを待ちます。

アプリケーション内の複数の場所からこのコンポーネントをレンダリングする場合、React はスタイルシートをドキュメントに 1 回だけ含めます:

function App() {
  return <>
    <ComponentOne />
    ...
    <ComponentOne /> // won't lead to a duplicate stylesheet link in the DOM
  </>
}

スタイルシートを手動でロードすることに慣れているユーザーにとって、これは、スタイルシートに依存するコンポーネントと一緒にスタイルシートの位置を特定する機会であり、より良いローカル推論を可能にし、実際に依存するスタイルシートのみをロードすることをより簡単にします。

スタイル・ライブラリやバンドラーとのスタイル統合もこの新機能を採用できるため、自分のスタイルシートを直接レンダリングしない場合でも、この機能を使用できるようにツールがアップグレードされれば、恩恵を受けることができます。

非同期スクリプトのサポート

HTMLでは、通常のスクリプト(<script src="...">)や遅延スクリプト(<script defer="" src="...">)はドキュメント順に読み込まれるため、コンポーネント・ツリーの奥深くでこの種のスクリプトをレンダリングするのは困難です。 しかし、非同期スクリプト(<script async="" src="...">)は任意の順序で読み込まれます。

React 19では、非同期スクリプトのサポートが強化され、スクリプト・インスタンスの再配置や重複排除を管理することなく、コンポーネント・ツリーの任意の場所、実際にスクリプトに依存するコンポーネントの内部でスクリプトをレンダリングできるようになりました。

function MyComponent() {
  return (
    <div>
      <script async={true} src="..." />
      Hello World
    </div>
  )
}

function App() {
  <html>
    <body>
      <MyComponent>
      ...
      <MyComponent> // won't lead to duplicate script in the DOM
    </body>
  </html>
}

すべてのレンダリング環境において、非同期スクリプトは重複排除され、複数の異なるコンポーネントでレンダリングされても、Reactはスクリプトを1度だけロードして実行します。

サーバーサイドレンダリングでは、非同期スクリプトは<head>に含まれ、スタイルシート、フォント、画像のプリロードなど、ペイントをブロックするより重要なリソースの後ろに優先されます。

リソースのプリロードをサポート

ドキュメントの初期ロード時やクライアントサイドの更新時に、可能な限り早い段階でロードする必要がありそうなリソースをブラウザに伝えることで、ページのパフォーマンスに劇的な影響を与えることができます。

React 19には、ブラウザ・リソースのロードとプリロードのための新しいAPIが多数含まれており、非効率的なリソース・ロードによって妨げられることのない優れたエクスペリエンスをできるだけ簡単に構築できるようになっています。

import { prefetchDNS, preconnect, preload, preinit } from 'react-dom'
function MyComponent() {
  preinit('https://.../path/to/some/script.js', {as: 'script' }) // このスクリプトをロードして熱心に実行する
  preload('https://.../path/to/font.woff', { as: 'font' }) // このフォントをプリロードする
  preload('https://.../path/to/stylesheet.css', { as: 'style' }) // このスタイルシートをプリロードする
  prefetchDNS('https://...') // このホストに実際に何もリクエストしていない場合
  preconnect('https://...') // 何かをリクエストしたいが、何をリクエストすればいいかわからないとき
}
<!-- 上記の場合、次のようなDOM/HTMLになります -->
<html>
  <head>
    <!-- リンク/スクリプトは、呼び出し順ではなく、早期ロードに役立つものから優先されます -->
    <link rel="prefetch-dns" href="https://...">
    <link rel="preconnect" href="https://...">
    <link rel="preload" as="font" href="https://.../path/to/font.woff">
    <link rel="preload" as="style" href="https://.../path/to/stylesheet.css">
    <script async="" src="https://.../path/to/some/script.js"></script>
  </head>
  <body>
    ...
  </body>
</html>

これらのAPIは、フォントのような追加リソースの発見をスタイルシートのロードの外に移動することによって、最初のページロードを最適化するために使用することができます。

また、予期されるナビゲーションで使用されるリソースのリストをプリフェッチし、クリック時やホバー時にもそれらのリソースを熱心にプリロードすることで、クライアントの更新をより速くすることができます。

サードパーティのスクリプトや拡張機能との互換性

サードパーティのスクリプトやブラウザの拡張機能を考慮し、ハイドレーションを改善しました。

ハイドレート時に、クライアントでレンダリングされる要素がサーバーからのHTMLで見つかった要素と一致しない場合、Reactはコンテンツを修正するためにクライアントの再レンダリングを強制します。

以前は、サードパーティのスクリプトやブラウザ拡張機能によって要素が挿入されると、ミスマッチエラーが発生し、クライアントがレンダリングされていました。

React 19では、<head><body>内の予期しないタグはスキップされ、ミスマッチエラーが回避されます。

Reactが無関係な水和のミスマッチのためにドキュメント全体を再レンダリングする必要がある場合、サードパーティのスクリプトやブラウザ拡張機能によって挿入されたスタイルシートはそのまま残されます。

より良いエラーレポート

React 19のエラー処理を改善し、重複をなくし、キャッチしたエラーとキャッチしていないエラーを処理するオプションを提供しました。

例えば、レンダリング中にエラー境界によって捕捉されたエラーがある場合、以前はReactはエラーを2回投げていました(元のエラーに対して1回、自動回復に失敗した後にもう1回)。

その結果、1つのエラーに対して3つのエラーが発生しました:
CleanShot 2024-12-10 at 16.46.16@2x.png

React 19では、すべてのエラー情報を含む単一のエラーログを記録します:
CleanShot 2024-12-10 at 16.46.38@2x.png

さらに、onRecoverableErrorを補完する2つの新しいルート・オプションを追加しました:

  • onCaughtError: Reactがエラー境界でエラーをキャッチしたときに呼び出されます。
  • onUncaughtError: Error Boundaryで捕捉されないエラーがスローされたときに呼び出されます。
  • onRecoverableError: エラーが発生したときに呼ばれ、自動的に回復する。

カスタム・エレメントのサポート

React 19はカスタム要素を完全にサポートし、Custom Elements Everywhereのすべてのテストに合格しています。

過去のバージョンでは、Reactでカスタム要素を使用することは困難でした。なぜなら、Reactは認識できないpropをプロパティではなく属性として扱うからです。

React 19では、以下のような戦略で、クライアントとSSR中に動作するプロパティのサポートを追加しました:

  • サーバサイドレンダリング:カスタム要素に渡されたpropsは、その型が文字列や数値のようなプリミティブな値であるか、値がtrueである場合、属性としてレンダリングされます。 objectsymbolfunction、またはvalue falseのような非プリミティブ型のpropsは省略されます。
  • クライアント側レンダリング:カスタム要素インスタンスのプロパティにマッチする小道具はプロパティとして割り当てられ、そうでない場合は属性として割り当てられます。

最後に

以上が、React 19安定版リリースノートの日本語訳となります。

個人的な感想ですが、Next.jsで先行実装されていたServer Components関連の機能が正式にReactの一部となったことで、今後は他のフレームワークへの展開も期待され、Reactエコシステム全体の進化を加速していきそうですね🥺

Discussion