出来る限り短く説明する React 19 のサンプルコード10選
VTeacher所属のMassです。
React 19 のサンプルコード
React 19 についてですが、2024年5月に RC 版が発表され、同年12月に安定版となりました。すでに2ヶ月以上が経過しており、目新しいものではありませんが、整理の意味も込めて記事にまとめました。
リポジトリ (GitHub)
Next.js
を前提としたサンプルコードを用意しました。 ver 15 です。
動作確認 (Vercel)
サンプルコードは Vercel
にデプロイしてあるので、動作確認にご利用ください。
サンプルコードの説明
useTransition
1.
useTransition自体はv18からありますが、v19からは、トランジション内で非同期関数を使用することで、送信中状態/エラー/フォーム/楽観的更新を自動的に処理するサポートが追加されています。これは何かというと、下記のサンプルコードで説明するなら、次の通りです。
1. startTransition
の処理開始 → isPending
が自動で true
2. startTransition
の処理終了 → isPending
が自動で false
これにより、わざわざisPendingのstateを別に容易する必要もなく、非同期処理をスッキリと記述できます。すごく便利。
import { useRouter, usePathname } from "next/navigation";
import { useState, useTransition } from "react";
async function fetchSample(name: string | null): Promise<string> {
return new Promise((resolve) => setTimeout(() => resolve('sample:' + name), 3000));
}
export default function Note() {
const [isPending, startTransition] = useTransition();
const [name, setName] = useState("");
const [sample, setSample] = useState<string | null>(null);
const router = useRouter();
const pathname = usePathname();
const handleSubmit = () => {
startTransition(async () => { // 👈startTransitionが開始したらisPendingは自動でtrue
const result = await fetchSample(name);
if (result.includes("error")) {
setSample(result);
return;
}
router.push(pathname);
}); // 👈startTransitionが終了したらisPendingは自動でfalse
};
return (
<div className="m-2">
<p className="text-xs text-gray-500 bg-white my-2">
Calling `startTransition` alone sets `isPending` to `true`. This allows for a clean way to handle asynchronous operations. While this is a fundamental process, the following chapters will focus on form-specific optimizations, introducing even more concise and simplified approaches.
</p>
<input type="text" className="border p-2 rounded" value={name} placeholder="name" onChange={(e) => setName(e.target.value)} />
{/* 👇isPendingの処理がスッキリする */}
<button onClick={handleSubmit} disabled={isPending} className="bg-blue-500 text-white p-2 rounded">
{isPending ? "Pending..." : "Post"}
</button>
{sample && <p>{sample}</p>}
{isPending && <span className="w-4 h-4 border-2 border-gray-300 border-t-transparent rounded-full animate-spin inline-block"></span>}
</div>
);
}
動作確認
useActionState
2.
useActionStateはformに特化し、formの定番処理をまとめて簡略化した(useTranstionさえも簡略化した)フックです。useActionStateを使うことで、formの処理がかなりスッキリと記述できます。form周りはuseActionStateを第一選択肢にしたほうが無難かと思います。
useActionStateの使用方法
const [
state, // formの状態、中身は次のformActionの戻り値
formAction, // formのactionに渡せるようにラップされた、formイベント発火時に呼ばれる関数
isPending // 送信中など、formの進行中を確認するフラグ
] = useActionState(
fn, // formActionの中身(これはラップされる)※サーバー関数OK
initialState, // formの状態の初期値
permalink? // オプション、fnがサーバー関数のときは設定が必要の可能性
);
// Using the useActionState hook to manage the state of actions and handle asynchronous operations concisely
import { useActionState } from "react";
async function fetchSample(name: string | null): Promise<string> {
return new Promise((resolve) => setTimeout(() => resolve(name ?? ""), 3000));
}
export default function Note() {
// 👇useActionStateフックを使用
const [message, submitAction, isPending] = useActionState<string | null, FormData>(
async (_prevState: string | null, formData: FormData) => { // 👈formActionの中身、submitボタンクリック時の処理(ここはラップされてsubmitActionになる)
const resMessage = await fetchSample(formData.get("name") as string | null);
return resMessage; // 👈formの状態を返す
},
null // 👈formの状態の初期値
);
return (
{/* 👇formのactionにuseActionStateから返されたsubmitActionを渡す */}
<form action={submitAction} className="m-2">
<p className="text-xs text-gray-500 bg-white my-2">
This is a hook that consolidates standard form processing. By using `useActionState`, form handling can be written in a much cleaner and more concise way.
</p>
<input type="text" className="border p-2 rounded" name="name" placeholder="name" />
<button type="submit" disabled={isPending} className="bg-blue-500 text-white p-2 rounded">
{isPending ? "Pending..." : "Post"}
</button>
{isPending && <span className="w-4 h-4 border-2 border-gray-300 border-t-transparent rounded-full animate-spin inline-block"></span>}
{message && <p>{message}</p>}
</form>
);
}
動作確認
<form>
アクション
3.
すでにuseActionStateの例で見ましたが、formのactionに関数を渡すことができます。
<form action={actionFunction}>
これはuseActionStateを使用する場合に限らず、独自に定義した関数を渡してもOKです。formに入力された値はformDataを経由してアクセスできます。useActionStateの定番なテンプレートでは扱えない場合に使用します。
// How to use the new action of the <form> element in React DOM and how to communicate with the server
import { useState } from "react";
async function fetchSample(name: string | null): Promise<string> {
return new Promise((resolve) => setTimeout(() => resolve(name ?? ""), 3000));
}
export default function Note() {
const [message, setMessage] = useState("");
// <form>のactionに渡す関数
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const resMessage = await fetchSample(formData.get("name") as string);
setMessage(resMessage);
}
return (
<form onSubmit={handleSubmit} className="m-2">
<p className="text-xs text-gray-500 bg-white my-2">
This is a method to connect custom functions with a form. You can access form values via `formData`. It is useful when `useActionState` cannot handle a case or when standard processing is not sufficient.
</p>
<input type="text" className="border p-2 rounded" name="name" placeholder="name" required />
<button type="submit" className="bg-blue-500 text-white p-2 rounded">
Post
</button>
<p>{message}</p>
</form>
);
}
動作確認
useFormStatus
4.
デザイン(UI設計やコンポーネント分割など)の都合を配慮したフックです。isPendingといったpropsを、いわゆるバケツリレーして深く深く渡していく必要はありません(しても良いですが)。useFormStatusでformの状態を子コンポーネントで取得できます。
// How to manage form status (posting, error, etc.) using the useFormStatus hook
import { useFormStatus } from "react-dom";
async function fetchSample(name: string | null): Promise<string> {
return new Promise((resolve) => setTimeout(() => resolve(name ?? ""), 3000));
}
// 👇子コンポーネント内で<form>の処理進行状況を取得できる
function SubmitButton() {
const { pending } = useFormStatus();
return <button type="submit" disabled={pending} className="bg-blue-500 text-white p-2 rounded">{pending ? "Posting..." : "Post"}</button>;
}
export default function Note() {
async function handleSubmit(formData: FormData) {
const name = formData.get("name") as string | null;
await fetchSample(name);
}
return (
<form action={handleSubmit} className="m-2">
<p className="text-xs text-gray-500 bg-white my-2">
You can retrieve the form state within a child component. There is no need to pass props like `isPending`.
</p>
<input type="text" className="border p-2 rounded" name="name" placeholder="name" required />
<SubmitButton />
</form>
);
}
form内であることを自動検出し、そのスコープ内のステータスとして取得できます。
動作確認
useOptimistic
5.
非同期処理の進行中に、「とりあえず表示しておきたい値」を即座に設定できるというフックです。楽観的な更新。useStateのタイミングよりも速くレンダリングされます。
// How to implement optimistic UI updates using the useOptimistic hook to improve user experience
import { useOptimistic, startTransition } from "react";
import { useState } from "react";
async function fetchSample(name: string | null): Promise<string> {
return new Promise((resolve) => setTimeout(() => resolve(name ?? ""), 3000));
}
export default function Note() {
const [name, setName] = useState<string | null>(null);
const [optimisticName, setOptimisticName] = useOptimistic<string | null>(
name
);
async function handleUpdate() {
startTransition(async () => {
setOptimisticName("■■■"); // 👈 setName()よりも速いタイミングで更新される
const result = await fetchSample("Bob");
setName(result);
});
}
return (
<div className="m-2">
<p className="text-xs text-gray-500 bg-white my-2">
You can instantly display values you want to show for now (optimistic updates). It renders even faster than the timing of `useState`.
</p>
<button onClick={handleUpdate} className="bg-blue-500 text-white p-2 rounded">
Show Name
</button>
<p className="mt-2">{optimisticName}</p>
</div>
);
}
動作確認
use
6. useはフックと同じように使えます。フックと同様、レンダー中のみに呼び出せます。一部フックと異なる点は、useは条件分岐の中でも呼び出すことが可能です。よくある誤解として「useはawaitの代わりに使う関数」とありますが、useはレンダー中のみ期待通りに動作します。
通常、サーバーコンポーネントではfetchを使いますが、そのfetchにawaitを使うとレンダリングがブロックされ、Suspenseをうまく活用できません。しかし、useの登場により、Promiseのままクライアントコンポーネントに渡しても、Suspenseを活用し、待機中の表示を容易に制御できるようになりました。
"use client";
// How to efficiently handle asynchronous data or resources using the use hook
import { Suspense, use } from "react";
function Message({ messagePromise }: { messagePromise: Promise<string> }) {
const message = use(messagePromise);
return <p>Here is the message: {message}</p>;
}
export default function Note({ messagePromise }: { messagePromise: Promise<string> }) {
return (
<div className="m-2">
<p className="text-xs text-gray-500 bg-white my-2">
Normally, `fetch` is used in server components, but using `await` here blocks rendering and prevents effective use of `Suspense`. However, with the introduction of `use`, passing a `Promise` directly to a client component has become a smart solution, making it easier to control loading states using `Suspense`.
</p>
<Suspense fallback={<p>Waiting for message...</p>}>
<Message messagePromise={messagePromise} />
</Suspense>
</div>
);
}
動作確認
useActionState
と useOptimistic
7. useActionStateとsetOptimisticNameを組み合わせた例です。
// How to smoothly integrate optimistic UI and asynchronous processing using useActionState and useOptimistic
import { useActionState, useOptimistic, useState } from "react";
async function fetchSample(name: string | null): Promise<string> {
return new Promise((resolve) => setTimeout(() => resolve(name ?? ""), 3000));
}
export default function Note() {
const [name, setName] = useState("");
const [optimisticName, setOptimisticName] = useOptimistic<string | null>("");
const [error, submitAction, isPending] = useActionState<string | null, FormData>(
async (_prevState: string | null, formData: FormData) => {
const newName = formData.get("name") as string | null;
if (newName === '') {
return "Error: required";
}
setOptimisticName(newName);
await fetchSample(newName);
setName(newName ?? "");
return null;
},
null
);
return (
<form action={submitAction} className="m-2">
<p className="text-xs text-gray-500 bg-white my-2">
An example combining `useActionState` and `setOptimisticName`.
</p>
<p className="mt-2">Name: {optimisticName || name}</p>
<input type="text" name="name" placeholder="name" className="border p-2 mb-2 rounded" />
<button type="submit" disabled={isPending} className="bg-blue-500 text-white p-2 rounded">
{isPending && <span className="w-4 h-4 border-2 border-gray-300 border-t-transparent rounded-full animate-spin inline-block" />}
Post
</button>
{error && <p className="text-red-500">{error}</p>}
</form>
);
}
動作確認
8. React Server Components
defaultはサーバーコンポーネントになります。ソースにusersが含まれていないことを確認してください。Chromeなら右クリックで「検証」を開き、ソースを確認できます。ページ内の http://localhost:3000/_next/static/chunks/app/lesson08-react-server-components/page.js をチェックしてみてください。use client をつけるとusersのデータがそのまま含まれ、suzuki@example.comのような個人情報が見えてしまいます。
// "use client"; // Note: This makes it a Client component.
// How to render components on the server side using React Server Components
const users = [
{
id: "1",
name: "Suzuki",
email: "suzuki@example.com",
},
{
id: "2",
name: "Satou",
email: "tanaka@example.com",
},
];
export default function Page() {
return (
<div className="text-center m-2">
<p className="text-xs text-gray-500 bg-white my-2">
By default, it is a server component. Make sure that users is not included in the source code. In Chrome, you can right-click and select Inspect to check the source. Try checking the following file within the page: http://localhost:3000/_next/static/chunks/app/lesson08-react-server-components/page.js. If you add use client, the users data will be directly included, exposing personal information such as suzuki@example.com.
</p>
<h1>This is server side.</h1>
<p>users: {users.length}</p>
</div>
);
}
- 参考
React チームのフルスタックアーキテクチャビジョンに含まれる機能
https://ja.react.dev/learn/start-a-new-react-project#which-features-make-up-the-react-teams-full-stack-architecture-vision
動作確認
9. Server Functions
クリックなどのイベントから、サーバー側でのみ実行可能な関数を呼び出せます。
以下は、ボタンをクリックするとサーバーの uptime
が表示されるコードです。
"use server";
import { exec } from "child_process";
import { promisify } from "util";
const stat = promisify(exec) as (command: string) => Promise<{ stdout: string; stderr: string }>;
export async function action(): Promise<string> {
const { stdout } = await stat("uptime");
return stdout.trim();
}
// How to efficiently process data while interacting with the server using server functions
"use client";
import { action } from "../actions";
export default function Note() {
async function handleClick() {
const stat = await action();
alert(stat);
}
return (
<div className="m-2">
<p className="text-xs text-gray-500 bg-white my-2">
You can call functions that can only be executed on the server from events like clicks. When you click, the server uptime will be displayed.
</p>
<button onClick={handleClick} className="bg-blue-500 text-white p-2 rounded">
Server Action
</button>
</div>
);
}
動作確認
ref
10. propsから
React 19 の改善点の一つとして、refにpropsとしてアクセスできるようになりました。
(これによりforwardRefは、将来的に非推奨になります)
// One of the improvements in React 19 is the ability to access refs as props
"use client";
import { useRef, forwardRef } from "react";
const ChildInputPropRef = ({ placeholder, ref }: { placeholder: string, ref: React.Ref<HTMLInputElement> }) => (
<input ref={ref} type="text" placeholder={placeholder} className="border p-2 rounded" />
);
const ChildInputForwardRef = forwardRef<HTMLInputElement, { placeholder: string }>(
({ placeholder }, ref) => (
<input ref={ref} type="text" placeholder={placeholder} className="border p-2 rounded" />
)
);
ChildInputForwardRef.displayName = "ChildInputForwardRef";
export default function Note() {
const inputForwardRef = useRef<HTMLInputElement>(null);
const inputPropRef = useRef<HTMLInputElement>(null);
return (
<div className="flex flex-col items-center gap-4 p-4 m-2">
<p className="text-xs text-gray-500 bg-white my-2">
One of the improvements in React 19 is that refs can be accessed as props. `forwardRef` is no longer needed.
</p>
<input type="text" placeholder="Normal" className="border p-2 rounded" />
<ChildInputForwardRef placeholder="ForwardRef" ref={inputForwardRef} />
<ChildInputPropRef placeholder="PropRef" ref={inputPropRef} />
<button
onClick={() => inputForwardRef.current?.focus()}
className="bg-blue-500 text-white p-2 m-2 rounded"
>
ForwardRef focus()
</button>
<button
onClick={() => inputPropRef.current?.focus()}
className="bg-blue-500 text-white p-2 m-2 rounded"
>
PropRef focus()
</button>
</div>
);
}
さらなる改善として、refもuseEffectのように cleanup
ができるようになりました。
<input
ref={(ref) => {
// ref created
// NEW: return a cleanup function to reset
// the ref when element is removed from DOM.
return () => {
// ref cleanup
};
}}
/>
Discussion