React 19 正式リリース

# はじめに

こんにちは、梅雨です。

2024年12月5日、React 19が安定版としてリリースされたとReact Teamからアナウンスがありました。

React 19はRC版が今年の4月に公開されていましたが、そこから追加の機能もあるので、今回は改めて新バージョンのReactでできるようになったことを紹介していきたいと思います。

https://react.dev/blog/2024/12/05/react-19

:::note warn
本記事のサンプルコードはTypeScriptでの実装となります。JavaScriptでの実装が見たい方は上記のリリースノートからご確認ください。
:::

# Actions

まずは、**Actions（アクションズ）** という概念が導入されました。アクションズとはトランジションのために用いられる非同期関数のことを指します。

まずは従来のフォームコンポーネントを見てみましょう。`name`、`error`、`isPending`それぞれのステートを`useState()`フックで管理する、よく見慣れた書き方だと思います。

```tsx:form.tsx
const UpdateName = () => {
  const [name, setName] = useState("");
  const [error, setError] = useState("");
  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では、以下のように`isPending`ステートを`useTransition()`フックによって管理できるようになりました。`useTransition()`フック自体はReact 18で実装されていましたが、引数は同期関数しか取ることができませんでした。

今回のアップデートでアクションズを引数にできるようになったため、トランジションの終わるタイミングで簡単にUIコンポーネントの再レンダリングをトリガーできるようになります。

```tsx:form.tsx
const UpdateName = () => {
  const [name, setName] = useState("");
  const [error, setError] = useState("");
  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>
  );
};

```

アクションズは後述の`useActionState()`フックおよび`useOptimistic()`でも使用されるため、ぜひ使えるようにしておきたいところです。

# `<form>` Actions & `useActionState()`

アクションズはReact 19の`react-dom`で新たに追加された`<form>`の機能とも統合されています。`action`propsにアクションを渡すことで、フォーム内に配置された`<input>`と`<button>`の制御を行うことができます。

一方で、`useActionState()`フックは`useTransition()`では別々に行っていた`isPending`ステートと`error`ステートの管理を一括で行ってくれます。このフックを使うと、ラップされたアクションをフォームのアクションに渡すだけで簡単にボタンやエラー文のUIを更新することができます。

```tsx:form.tsx
const ChangeName = () => {
  const [error, submitAction, isPending] = useActionState(
    async (previousState: string | null, formData: FormData) => {
      const error = await updateName(formData.get("name") as string);
      console.log(error);
      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>
  );
};
```

`useActionState()`フックの引数となるアクションは`previousState`と`formData`の2つを引数とします。`formData`は標準APIのオブジェクトであり、各要素のnameプロパティをキーとして値を取得することができます。

:::note warn
`useActionState()`フックはカナリアリリースでは`react-dom`の`useFormState()`フックとして提供されていました。React 19では`useFormState()`フックは廃止されているため、注意してください。
:::

# `useFormStatus()`

`useFormStatus()`フックはデザインシステムにおいてフォーム内のUIコンポーネントを分離して管理したい時に用いることができます。このフックが呼ばれると、DOMツリーの親をたどっていき、formが見つかるとその状態にアクセスすることができます。

このフックを用いると、先ほどのフォームは以下のように書くことができます。

```tsx:ui/button.tsx
const DesignButton = ({ children }: { children: string }) => {
  const { pending } = useFormStatus();
  return <button type="submit" disabled={pending}>{children}</button>;
};

export default DesignButton
```

```tsx:form.tsx
import DesignButton from "./ui/button.tsx";

const ChangeName = () => {
  const [error, submitAction] = useActionState(
    async (previousState: string | null, formData: FormData) => {
      const error = await updateName(formData.get("name") as string);
      console.log(error);
      if (error) {
        return error;
      }
      redirect("/path");
      return null;
    },
    null
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <DesignButton>Update</DesignButton>
      {error && <p>{error}</p>}
    </form>
  );
};
```

同様の実装はバケツリレーやコンテクストによっても実現できますが、`useFormStatus()`フックではよりシンプルに記述することができます。

# `useOptimistic()`

今回のアップデートではさらにOptimistic Update（楽観的更新）をサポートする`useOptimistic()`フックが追加されました。

楽観的更新とは、リクエストを送信した際にレスポンスが成功することを"楽観的に"期待してUIの更新を行うことです。使い方によってはUXの向上が見込めます。

注目して欲しいのは`submitAction`アクション内で`setOptimisticName`を呼んでいる部分です。この`setOptimisticName`によって更新された値はトランジションが終わるまで`optimisticName`としてアクセスできますが、トランジションが終了するとpropsで渡ってきている`currentName`の値に置き代わります。

```tsx:form.tsx
const ChangeName = ({
  currentName,
  onUpdateName,
}: {
  currentName: string;
  onUpdateName: (name: string) => void;
}) => {
  const [optimisticName, setOptimisticName] = useOptimistic(currentName);

  const submitAction = async (formData: FormData) => {
    const newName = formData.get("name") as string;
    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}
        />
        <button type="submit">Update</button>
      </p>
    </form>
  );
};
```

`useOptimistic()`フックは第２引数にアクションを取るような使い方もあるので、実際に使用する際は以下のリファレンスを参考にしてみてください。

https://react.dev/reference/react/useOptimistic

# `use()`

`use()`フックはその名の通りさまざまな値を"使う"ことのできるフックです。主にPromiseオブジェクトやContextの値を利用したいときに使います。

`use()`フックの引数にPromiseオブジェクトを渡すと、その返り値はPromiseのawaitされた型になります。

注意点として、この`use()`フックはレンダー内で作られたPromiseオブジェクトを引数に取ることはできないので、propsとして`Suspense`コンポーネントなどの外から渡してあげる必要があります。

```tsx:comments.tsx
type Coment = {
  id: string;
  body: string;
};

const Comments = ({
  commentsPromise,
}: {
  commentsPromise: Promise<Coment[]>;
}) => {
  const comments = use(commentsPromise);

  return (
    <div>
      {comments.map((comment) => (
        <p key={comment.id}>{comment.body}</p>
      ))}
    </div>
  );
};

const Page = ({ commentsPromise }: { commentsPromise: Promise<Coment[]> }) => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  );
};
```

`use()`フックの引数にコンテクストを渡すと、その返り値はコンテクストの値となります。なので、コンポーネントのトップレベルで使用した場合は`useContext()`フックと同様の挙動になります。一方で、`use()`フックはif文などの中でも呼べる点で異なります。

```tsx:ui/heading.tsx
import { JSX, use } from "react";
import ThemeContext from "./ThemeContext";

const Heading = ({ children }: { children: JSX.Element }) => {
  if (children == null) {
    return null;
  }

  const theme = use(ThemeContext);
  return <h1 style={{ color: theme.color }}>{children}</h1>;
};
```

# `prerender` & `prerenderToNodeStream`

`react-dom/static`の新たなAPIとして、`prerender`および`prerenderToNodeStream`が追加されました。これらのAPIはSSGのために使用され、Reactツリーから静的なHTMLを生成することができます。

```tsx
import { prerender } from "react-dom/static";

const handler = async (request: Request) => {
  const { prelude } = await prerender(<App />, {
    bootstrapScripts: ["/main.js"],
  });

  return new Response(prelude, {
    headers: { "content-type": "text/html" },
  });
};
```

ストリームではデータを分割して処理するため、適切な使い方をすればパフォーマンスを改善することができます。

# Server Components

Next.jsなどで使用している方も多いと思いますが、サーバー環境でコンポーネントをレンダリングできる機能です。

今まではサーバーからデータをフェッチする際、クライアントで`useEffect()`フックを用いて取得を行うのが一般的でした。

```tsx
const Page = () => {
  const [content, setContent] = useState("");

  useEffect(() => {
    fetch("/api/contents")
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        setContent(data.content);
      });
  }, []);

  return <div>{content}</div>;
};
```

サーバーコンポーネントでは、コンポーネントを非同期にすることで`fetch`関数自体をawaitすることができます。

```tsx
const Page = async () => {
  const res = await fetch("/api/contents");
  const data = await res.json();

  return <div>{data.content}</div>;
};
```

クライアントにはサーバーでレンダリングされたHTMLのみが送信されるため、使い所によってはパフォーマンスの改善が望めます。

# Server Actions

サーバーコンポーネントと同様、Next.jsで使用している方も多いと思いますが、こちらは非同期の関数（アクション）をサーバーで実行できる機能です。

`"user server"`ディレクティブによりサーバーアクションが定義されると、サーバーは関数への参照をクライアントに返し、クライアントはアクションの実行時にサーバーにリクエストを送信します。

```tsx
import Button from "./Button";

const EmptyNote = () => {
  async function createNoteAction() {
    "use server";
    
    await db.notes.create();
  }

  return <Button onClick={createNoteAction} />;
}
```

# その他の追加の機能

ここからはテンポよく紹介していきます。

## `ref`のpropsでの利用

今までコンポーネントに`ref`オブジェクトを渡す際は`forwardRef`を用いる必要がありましたが、今回のアップデートで`ref`を直接コンポーネントのpropsに渡すことができるようになりました。

## ハイドレーションエラーの表示改善

ハイドレーションエラーの表示が差分形式の表示となり見やすくなりました。

## Contextコンポーネント

今までは`<Context.Provider>`として使用していたContextのコンポーネントを`<Context>`として使えるようになりました。

## `ref`のクリーンアップ関数

`useEffect`などと同様に、`ref`でもクリーンアップ関数を指定できるようになりました。これらのクリーンアップ関数はアンマウント時に実行されます。

## `useDeferredValue()`フックの初期値の指定

`useDeferredValue()`フックで初期値を指定できるようになりました。

このフックに関してはあまり聞き馴染みのない方も多いと思いますが（筆者も初めて聞きました）、高優先度のユーザーインタラクションを妨げることなく、低優先度の状態更新を遅延させることができるフックのようです。

## metaタグのサポート

以前までのバージョンではmetaタグの管理をReact側から行うことができませんでしたが、アップデートによりコンポーネント内からmetaタグの記述ができるようになりました。これらはストリーミングSSRやサーバーコンポーネントでも使用することができます。

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

上記のmetaタグのサポートにより、コンポーネント内にlinkタグを記述できるようになりました。そこで生じるのがスタールシートの優先順位問題です。

React 19ではlink要素とstyle要素に`precedence`属性を付与することで優先順位を指定することができます。また、`precedence`属性が記述された要素が読み込まれるまでレンダリングをサスペンドすることができます。

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

scriptタグにも新たなサポートが追加され、コンポーネント内で非同期に読み込まれたスクリプトが完全にロードされるまでレンダリングをサスペンドすることができるようになりました。

また、複数のコンポーネントで同一のスクリプトがロードされる際は、Reactがこれを解決して1回のロードにしてくれます。

## React DOM API

React 19では新たに

- prefetchDNS
- preconnect
- preload
- preloadModule
- preinit
- preinitModule

の６つのAPIが追加されました。これらのAPIを用いてリソースを先に読み込んでおくことでWebページ高速化が期待できます。

上記の６つのうち、初めの４つはlinkタグのrel属性に指定するのと同等の動きをし、下の２つはリソースの読み込みに加えて実行までを行います。

## サードパーティモジュールとの共存

今まではサードパーティモジュールによって要素が要素が挿入された際、意図しないミスマッチエラーが発生してしまっていました。

React 19ではこのようなミスマッチエラーが回避され、ドキュメント全体が再レンダリングされる時はサードパーティモジュールによって挿入された要素やスタイルシートはそのまま残されるようになりました。

## エラーレポートの改善

以前までのバージョンでは重複して表示されていたエラーレポートの表示が、まとまって１つのエラーレポートとなりました。

また、`createRoot`関数のオプションでキャッチされたエラー、キャッチされていないエラー、自動でリカバーされたエラーのそれぞれに対して個別の挙動を指定できるようになりました。

## カスタム要素のサポート

新しくカスタム要素がサポートされました。以前はReactによって認識されないpropsがプロパティとしてではなく属性として扱われてしまっていたことから困難でしたが、いくつかの戦略によりサポートが実現されました。

# おわりに

以上が今回発表されたReact 19の全容となります。Reactはバージョンの移り変わりが非常に早いので、近い将来に今回発表された機能も標準的になっていくと思います。

今回の記事の内容をおさえて、ぜひ快適なフロントエンド生活をお楽しみください。