はじめに
こんにちは、梅雨です。
2024年12月5日、React 19が安定版としてリリースされたとReact Teamからアナウンスがありました。
React 19はRC版が今年の4月に公開されていましたが、そこから追加の機能もあるので、今回は改めて新バージョンのReactでできるようになったことを紹介していきたいと思います。
本記事のサンプルコードはTypeScriptでの実装となります。JavaScriptでの実装が見たい方は上記のリリースノートからご確認ください。
Actions
まずは、Actions(アクションズ) という概念が導入されました。アクションズとはトランジションのために用いられる非同期関数のことを指します。
まずは従来のフォームコンポーネントを見てみましょう。name
、error
、isPending
それぞれのステートをuseState()
フックで管理する、よく見慣れた書き方だと思います。
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コンポーネントの再レンダリングをトリガーできるようになります。
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を更新することができます。
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プロパティをキーとして値を取得することができます。
useActionState()
フックはカナリアリリースではreact-dom
のuseFormState()
フックとして提供されていました。React 19ではuseFormState()
フックは廃止されているため、注意してください。
useFormStatus()
useFormStatus()
フックはデザインシステムにおいてフォーム内のUIコンポーネントを分離して管理したい時に用いることができます。このフックが呼ばれると、DOMツリーの親をたどっていき、formが見つかるとその状態にアクセスすることができます。
このフックを用いると、先ほどのフォームは以下のように書くことができます。
const DesignButton = ({ children }: { children: string }) => {
const { pending } = useFormStatus();
return <button type="submit" disabled={pending}>{children}</button>;
};
export default DesignButton
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
の値に置き代わります。
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
コンポーネントなどの外から渡してあげる必要があります。
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文などの中でも呼べる点で異なります。
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はバージョンの移り変わりが非常に早いので、近い将来に今回発表された機能も標準的になっていくと思います。
今回の記事の内容をおさえて、ぜひ快適なフロントエンド生活をお楽しみください。