6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReactAdvent Calendar 2024

Day 6

React 19 正式リリース

Last updated at Posted at 2024-12-08

はじめに

こんにちは、梅雨です。

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

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

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

Actions

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

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

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コンポーネントの再レンダリングをトリガーできるようになります。

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>の機能とも統合されています。actionpropsにアクションを渡すことで、フォーム内に配置された<input><button>の制御を行うことができます。

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

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

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

useFormStatus()

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

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

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

export default DesignButton
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の値に置き代わります。

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()フックは第2引数にアクションを取るような使い方もあるので、実際に使用する際は以下のリファレンスを参考にしてみてください。

use()

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

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

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

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文などの中でも呼べる点で異なります。

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を生成することができます。

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()フックを用いて取得を行うのが一般的でした。

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することができます。

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

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

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

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

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

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

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

エラーレポートの改善

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

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

カスタム要素のサポート

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

おわりに

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

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

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?