100秒テックの「React 19 がでた」を見たので、チートシートを作成してみました
はじめに
今回は、いつも楽しく拝見させていただいている「100秒テック」のakiさんとコラボしました!
まずは、とにかく動画をご覧ください!
他にも役立つフロントエンド関連の技術動画がたくさんあるので、いいねとチャンネル登録をよろしくね!(言いたかったw)
この記事では、タイトル通り、React19のチートシートとして活用いただけるように仕上げました。またサンプルコードは、できるだけ理解しやすいようシンプルにして、React環境にコピー&ペーストするだけで動作するようにしました。
動画と合わせて、この記事もReact 19の理解を深めるのに役立てていただけると嬉しいです。ぜひ、最後までご覧ください!
アクション
アクションは、React 19を理解する上で欠かせない重要な概念です。このアクションを基盤として、様々な機能が追加されています。そのため、React 19で追加された機能を活用するにあたっては、アクションを理解しておくことをおすすめします。この章では、アクションの仕組みと背景について説明します。
公式ドキュメントでは、アクションを次のように定義しています。
非同期トランジションを使用する関数を規約として「アクション」と呼ぶ
ではまず、Reactにおけるトランジションとは何でしょうか?
トランジションというと、フロントエンド開発者であれば、CSSのtransitionやView Transitions APIを想像してしまうかもしれませんが、それらとは異なります。
これはReact 18で導入された概念です。
これも公式ドキュメントによると、次のように定義しています。
トランジション(transition; 段階的推移)とは React における新たな概念であり、緊急性の高い更新 (urgent update) と高くない更新 (non-urgent update) を区別するためのものです。
・緊急性の高い更新とはタイプ、クリック、プレスといったユーザ操作を直接反映するものです。
・トランジションによる更新は UI をある画面から別の画面に段階的に遷移させるものです。
タイプ、クリック、プレスのような緊急性の高い更新は、物理的な物体の挙動に関する我々の直観に反しないよう、即座に反応する必要があり、そうでないと「おかしい」と認識されてしまいます。一方でトランジション内では、ユーザは画面上であらゆる中間の値が見えることを期待していません。
Reactにおいてトランジションとは、ユーザー体験をスムーズに保ちながら、非同期処理やUI更新を行うための仕組みです。トランジションを使うと、Reactは「何をすぐに更新するか(緊急)」と「何を後で更新するか(優先度が低い)」を分けて処理できます。
この概念を具現化したものがuseTransition
フックやstartTransition
APIです。
const [isPending, startTransition] = useTransition();
const handleSubmit = () => {
startTransition(() => {
// ステート更新処理を行う
setSomeState();
});
}
このstartTransition
のコールバック内で行われたステート更新はトランジションとなり、優先度の低い更新として扱われます。
React 18ではこのコールバック関数は同期関数しか扱えませんでしたが、React 19で非同期関数も扱えるようになり、これをアクションと定義した。ということです。
const [isPending, startTransition] = useTransition();
const handleSubmit = async () => {
startTransition(async () => { // React 19で非同期関数の使用が可能になった
// 非同期のステート更新処理を行う
await someAsyncOperation();
});
};
useActionState フック
概要
useActionState
は、アクションの実行 (useTransition
のような機能) と、実行結果に応じた状態管理 (useReducer
のような機能) を1つのフックで実現するためのものです。
アクション実行のみが必要な場合は useTransition
で十分ですが、アクションの結果に基づいて状態を更新したい場合 に useActionState
が役立ちます。
シグネチャ
const [state, formAction, isPending] = useActionState(action, initialState, parmalink?);
引数
引数 | 説明 |
---|---|
action | 実行するアクション |
initialState | Stateの初期値 |
parmalink (optional) | フォームアクションの結果に基づいて遷移するURLを指定 |
戻り値
戻り値 | 説明 |
---|---|
state | 現在のState |
formAction | アクションを実行するための関数 |
isPending | トランジションが保留中であるかどうかを示すフラグ |
サンプルコード
import { useActionState } from "react";
const incrementLike = async (currentCount: number): Promise<number> => {
return await new Promise((resolve) =>
setTimeout(() => resolve(currentCount + 1), 1000)
);
};
export const LikeCounter: React.FC = () => {
const [likeCount, likeCountAction, isPending] = useActionState(
incrementLike,
0
);
return (
<form>
<div>{isPending ? "Updating…" : likeCount}</div>
<button formAction={likeCountAction}>Like</button>
</form>
);
};
useActionState
とuseState
+useTransition
の挙動の違いについて
補足: ちなみにですが、上記サンプルコードをuseActionState
を使わずに実装しようとすると、以下のようにuseState
とuseTransition
を使って実現する方法が考えられます。
import { useTransition, useState } from "react";
const incrementLike = async (currentCount: number): Promise<number> => {
return await new Promise((resolve) =>
setTimeout(() => resolve(currentCount + 1), 1000)
);
};
export const LikeCounter: React.FC = () => {
const [likeCount, setCount] = useState(0);
const [isPending, startTransition] = useTransition();
const likeCountAction = () => {
startTransition(async () => {
const updatedLikeCount = await incrementLike(likeCount);
setCount(updatedLikeCount);
});
};
return (
<form>
<div>{isPending ? "Updating…" : likeCount}</div>
<button formAction={likeCountAction}>Like</button>
</form>
);
};
実行してみるとわかるのですが、同じ挙動にはなりません。
useActionState
を使用した場合、ボタンを連打しても、クリックした回数だけ非同期処理が実行され、各処理の結果が順番に反映されます。
しかし、useState
とuseTransition
を組み合わせて実装した場合、likeCount
をこのコンポーネントで管理しているので、ボタンを連打すると、トランジションの実行結果を反映するわけではなく、あくまで現在の状態を基に更新しようとします。クリックは連打した回数分実行されますが、同一トランジション内では、同じ結果が返ってきます。そのためクリックが防がれているような挙動に見えます。
同じような処理をuseState
とuseTransition
で実装しようとすると、かなり複雑な実装になります。
つまり、useActionState
は、非同期処理を伴う状態更新をより安全かつ適切に行うためのフックなのです。
公式ドキュメント
<form> アクション
概要
react-dom
の<form>のaction属性にアクションが渡せるようになりました。これによりフォーム送信時にアクションを実行できるようになりました。
使い方
<form>のaction属性にアクションを指定します。
<form action={runAction}>
…
</form>
サンプルコード
import { useState } from "react";
export const ContactForm: React.FC = () => {
const [name, setName] = useState("");
async function handleSubmit(formData: FormData) {
const name = formData.get("name");
return `Hello, ${name}!`;
}
return (
<form
action={async (formData) => {
const result = await handleSubmit(formData);
setName("");
alert(result);
}}
>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
value={name}
placeholder="Your name"
onChange={(e) => setName(e.target.value)}
/>
</div>
<button type="submit">Send</button>
</form>
);
};
公式ドキュメント
useFormStatus フック
概要
現在のフォーム送信に関するステータス情報を提供するフックです。このフックを使用することで、コンポーネント間でPropsを介して状態を受け渡すProps Drilling、Propsのバケツリレーをせずにステータス情報を取得することができます。
使い方
import {useFormStatus} from 'react-dom';
function MyButton() {
const {pending} = useFormStatus();
return <button type="submit" disabled={pending} />
}
引数
なし
戻り値
戻り値 | 説明 |
---|---|
pending | 親Formで送信が進行中かを判定するフラグ |
data | FormData インターフェイスのオブジェクト |
method | 'get' or 'post'かのいずれかの文字列。HTTPメソッドのどちらで送信しているかを表す |
action | 親Formのpropsであるactionに渡された関数の参照 |
サンプルコード
import { useTransition, useState } from "react";
import { useFormStatus } from "react-dom";
const incrementLike = async (currentCount: number): Promise<number> => {
return await new Promise((resolve) =>
setTimeout(() => resolve(currentCount + 1), 1000)
);
};
const LikeButton = () => {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Processing…" : "Like"}</button>;
};
export const LikeCounter: React.FC = () => {
const [likeCount, setCount] = useState(0);
const [, startTransition] = useTransition();
const likeCountAction = async () => {
startTransition(async () => {
const updatedLikeCount = await incrementLike(likeCount);
setCount(updatedLikeCount);
});
};
return (
<form action={likeCountAction}>
<div>{likeCount}</div>
<LikeButton />
</form>
);
};
公式ドキュメント
useOptimistic フック
概要
UIを楽観的に更新するためのフックです。
サーバーのレスポンスを待たずに入力内容を即座に UI に反映させることができます。
シグネチャ
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);
引数
引数 | 説明 |
---|---|
state | 初期状態や、実行中のアクションが存在しない場合に返される値です |
updateFn(currentState, optimisticValue) | currentStateは現在のステート値です。optimisiticValueはaddOptimisticに渡された楽観的更新に使用する値です。updateFnはこれらの値を使用して、新しい楽観的ステートを返します。 |
戻り値
戻り値 | 説明 |
---|---|
optimisticState | 結果としての楽観的ステートです。実行中のアクションがない場合は ステートと等しくなり、何らかのアクションが実行中の場合はupdateFnが返す値と等しくなります。 |
addOptimistic | 楽観的な更新を行う際に呼び出すためのディスパッチ関数です。任意の型の引数のoptimisticValueを受け取ります。stateとoptimisticValueを引数にしてupdateFnが呼び出されステートが更新されます。 |
サンプルコード
import { useOptimistic, useState, useTransition } from "react";
const incrementLike = async (currentCount: number): Promise<number> => {
return new Promise((resolve) =>
setTimeout(() => resolve(currentCount + 1), 1000)
);
};
export const LikeCounter: React.FC = () => {
const [likeCount, setCount] = useState(0);
const [optimisticLikeCount, incrementLikeOptimistic] = useOptimistic(
likeCount,
(currentValue: number, optimisticValue: number) =>
currentValue + optimisticValue
);
const [isPending, startTransition] = useTransition();
const handleClick = () => {
startTransition(async () => {
incrementLikeOptimistic(1); // 楽観的に +1 する
const updatedLikeCount = await incrementLike(likeCount); // サーバーから値を取得
setCount(updatedLikeCount); // サーバからの値を state に反映
});
};
return (
<div>
<div>{optimisticLikeCount}</div>
<button disabled={isPending} onClick={handleClick}>
Like
</button>
</div>
);
};
公式ドキュメント
use API
概要
use APIは、プロミスやコンテキストなどのリソースから値を読み取るためのAPIです。
サーバーコンポーネントとクライアントコンポーネントで使用する際には、以下の注意点を参考にしてください。
シグネチャ
const value = use(resource);
引数
引数 | 説明 |
---|---|
resource | 値を読み取りたいプロミスまたはコンテキストを指定します。 |
戻り値
戻り値 | 説明 |
---|---|
value | プロミスで解決された値やコンテキストの値が返ってきます。 |
サンプルコード
補足: useを使わずにuseEffectで実現した場合
useEffectを使った場合は、useStateも必要になりますし、コード量が多くなりますね。
import { useEffect, useState } from "react";
const fetchData = async () => {
return new Promise<string>((resolve) => {
setTimeout(() => {
resolve("Hello from use!");
}, 1000);
});
};
export default function Example() {
const [data, setData] = useState<string | null>(null);
useEffect(() => {
fetchData().then((result) => {
setData(result);
});
}, []);
return (
<div>
<h2>use のサンプル</h2>
<p>データ: {data}</p>
</div>
);
}
公式ドキュメント
React Server Components(RSC)
概要
React Server Component (RSC)は、Reactの新しい機能で、サーバーサイドでレンダリングされるコンポーネントを作成できるものです。これにより、フロントエンドでのJavaScriptバンドルを最小限に抑え、サーバーサイドでのデータ取得や処理を効率的に行うことができます。
RSCの内容は非常に深いため、ここでは基本的な説明にとどめておきます。さらに詳しく知りたい方は、公式ドキュメントやその他のリソースを参照することをおすすめします。
サンプルコード
export default async function PostsPage() {
// 外部APIから記事データを取得する
const response = await fetch("https://example.com/posts");
// レスポンスがOKでなければエラーをthrow
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
// JSON形式でレスポンスを解析
const postDataList = await response.json();
return (
<div>
<h2>記事一覧</h2>
<ul>
{/* 取得した記事データをリスト表示 */}
{postDataList.map((post) => (
<li key={post.id}>
<a href={post.url}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}
公式ドキュメント
Server Actions
概要
Server ActionsはReactのクライアントコンポーネントから直接サーバーサイドのロジックを実行できる仕組みです。これにより、クライアントとサーバー間の通信が簡潔になり、アプリケーション全体のコードの一貫性が向上します。
クライアントから見たら、非同期関数であり、アクションの一種と考えることができます。
サンプルコード
公式ドキュメント
おわりに
今回は、React 19の改善点については詳しく触れませんでしたが、興味深い変更がいくつもありました。
例えば、refにpropsとしてアクセスできるようになったり、ref用のクリーンアップ関数が用意されたり、Contextのプロバイダーが従来の <Context.Provider> に加えて、<Context> だけで使用できるようになった点などが挙げられます。<Context.Provider>は非推奨になる予定です。
その他にも多くの改善点がありますので、お時間のあるときにぜひ確認してみてください。
ここまで読んでいただき、ありがとうございました🙏🏻🙏
あとakiさん、コラボ企画を許諾していただきありがとうございました!
また交流しましょうね🫡
Discussion