❄️

出来る限り短く説明する React 19 のサンプルコード10選

2025/02/25に公開

VTeacher所属のMassです。

React 19 のサンプルコード

React 19 についてですが、2024年5月に RC 版が発表され、同年12月に安定版となりました。すでに2ヶ月以上が経過しており、目新しいものではありませんが、整理の意味も込めて記事にまとめました。

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

リポジトリ (GitHub)

Next.js を前提としたサンプルコードを用意しました。 ver 15 です。

動作確認 (Vercel)

サンプルコードは Vercel にデプロイしてあるので、動作確認にご利用ください。

サンプルコードの説明

1. useTransition

https://ja.react.dev/reference/react/useTransition

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>
    );
}

動作確認

2. useActionState

https://ja.react.dev/reference/react/useActionState

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>
    );
}

動作確認

3. <form> アクション

https://ja.react.dev/blog/2024/12/05/react-19#form-actions

すでに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>
    );
}

動作確認

4. useFormStatus

https://ja.react.dev/reference/react-dom/hooks/useFormStatus

デザイン(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内であることを自動検出し、そのスコープ内のステータスとして取得できます。

動作確認

5. useOptimistic

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

非同期処理の進行中に、「とりあえず表示しておきたい値」を即座に設定できるというフックです。楽観的な更新。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>
    );
}

動作確認

6. use

useはフックと同じように使えます。フックと同様、レンダー中のみに呼び出せます。一部フックと異なる点は、useは条件分岐の中でも呼び出すことが可能です。よくある誤解として「useはawaitの代わりに使う関数」とありますが、useはレンダー中のみ期待通りに動作します。

https://ja.react.dev/reference/react/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>
    );
}

動作確認

7. useActionStateuseOptimistic

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

https://ja.react.dev/reference/rsc/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>
    );
}

動作確認

9. Server Functions

https://ja.react.dev/reference/rsc/server-functions

クリックなどのイベントから、サーバー側でのみ実行可能な関数を呼び出せます。
以下は、ボタンをクリックするとサーバーの uptime が表示されるコードです。

actions.tsx
"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();
}
/components/note.tsx
// 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>
    );
}

動作確認

10. propsから ref

https://ja.react.dev/blog/2024/12/05/react-19#ref-as-a-prop

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