📝

React19Betaで紹介された新しいhookとAPIをまとめてみた

2024/04/29に公開

概要

https://react.dev/blog/2024/04/25/react-19 でReact19のBetaに関する記事が出されたので内容をまとめてみました。

React19のインストール方法

Reactプロジェクトの作成

yarn create vite react19-beta --template react-ts

参考

https://vitejs.dev/guide/#scaffolding-your-first-vite-project

React19Betaのインストール

cd react19-beta
yarn add react@beta react-dom@beta

上記のコマンド実行後、package.jsonに以下を追記して下さい。

{
  "dependencies": {
    "@types/react": "npm:types-react@beta",
    "@types/react-dom": "npm:types-react-dom@beta"
  },
  "overrides": {
    "@types/react": "npm:types-react@beta",
    "@types/react-dom": "npm:types-react-dom@beta"
  }
}

参考

https://react.dev/blog/2024/04/25/react-19-upgrade-guide#installing

React19での新機能

Actions

フォームに値を入力しボタンをクリックすると、先頭にHelloが付けられた値が非同期通信で返され、その値を画面に表示する例を考えます。非同期通信中はボタンがdisable状態になるとします。

今までの例

const promise = (name: string) =>
  new Promise<string>((resolve) =>
    setTimeout(() => resolve(`Hello ${name}`), 1000),
  );


const Old = () => {
  const [value, setValue] = useState("");
  const [isPending, setIsPending] = useState(false);
  const [result, setResult] = useState("");

  const handleSubmit = async () => {
    setIsPending(true);
    const promiseValue = await promise(value);
    setResult(promiseValue);
    setIsPending(false);
  };

  return (
    <div>
      <input value={value} onChange={(event) => setValue(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        click
      </button>
      {result && <p>{result}</p>}
    </div>
  );
};

ボタンが押されたタイミングでisPendingをtrueにし、値が返ってきたらisPendingをfalseに戻してあげるという一般的なやり方です。isPendingというstateを管理し、非同期処理の開始・終了時にisPendingの変更も行う必要があります。

useTransitionを使用した例

const UseTransition = () => {
  const [value, setValue] = useState("");
  const [isPending, startTransition] = useTransition();
  const [result, setResult] = useState("");

  const handleSubmit = () => {
    startTransition(async () => {
      const promiseValue = await promise(value);
      setResult(promiseValue);
    });
  };

  return (
    <div>
      <input value={value} onChange={(event) => setValue(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        click
      </button>
      {result && <p>{result}</p>}
    </div>
  );
};

startTransitionにasync functionが渡せるようになり、isPendingのtrue・falseの更新を行う必要がなくなりました。

useActionStateを使用した例

const UseActionState = () => {
  const [result, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const promiseValue = await promise(formData.get("name"));
      return promiseValue;
    },
    "",
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>
        click
      </button>
      {result && <p>{result}</p>}
    </form>
  );
};

useActionStateで非制御コンポーネントを使用することにより、formの入力状態・isPendingの管理を一つのactionで行うことができるようになりました。

新hook:useActionState

先ほどの例で使用しました。関数(いわゆる"Action")と初期stateを引数に受け取り、["Action"で返される値、formactionpropsに渡す関数、isPending]が返されます。

React DOM:<form> Actions

自動的にActionによってformを送信できるように、formのactionprops、inputやbuttonのformActionpropsに関数を渡せるようになりました。
Actionは成功すれば非制御コンポーネントのフォームを自動的にリセットします。

React DOM:新hook:useFormStatus

useFormStatusは親のformのstatusを、formがContextProviderであるかのように読み取ることができます。
先ほどの例で考えてみます。

const UseActionState = () => {
  const [result, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const promiseValue = await promise(formData.get("name"));
      return promiseValue;
    },
    "",
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>
        click
      </button>
      {result && <p>{result}</p>}
    </form>
  );
};

この時、button部分を別のコンポーネントに切り出したい時、isPendingをpropsとして渡すことなくuseFormStatusを使用することで以下のように取得することができます。

const Button = () => {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      click
    </button>
  );
};

const UseActionState = () => {
  const [result, submitAction] = useActionState(
    async (previousState, formData) => {
      const promiseValue = await promise(formData.get("name"));
      return promiseValue;
    },
    "",
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <Button />
      {result && <p>{result}</p>}
    </form>
  );
};

新hook:useOptistimic

データを変更するにおいてよくあるUIパターンは、非同期リクエストが行われているときに楽観的に最終的なstateを表示することです。useOptistimicをこれを簡単にします。
ユーザがお気に入り登録・解除するときにAPIリクエストの結果を待つことなく、リクエストと同時にその結果をUIに反映させ、リクエストが成功すればそのまま、失敗すれば元の状態に戻す時などが考えられます。今回の例では簡易的にチェックボックスを使用し、useStateを使った例とuseOptistimicを使った例を試してみます。

useStateを使用した例

APIリクエストを行う関数は以下を使用します。ランダムにリクエストが成功もしくは失敗します。

const promise = (_: boolean) =>
  new Promise<void>((resolve, reject) => {
    if (Math.random() > 0.5) {
      setTimeout(() => {
        resolve();
      }, 1000);
    } else {
      setTimeout(() => {
        reject();
      }, 1000);
    }
  });

useStateを使用した例は以下のようになります。

const UseState = () => {
  const [checked, setChecked] = useState(false);
  const [pending, startTransition] = useTransition();
  const handleSubmit = () => {
    const newChecked = !checked;
    setChecked(newChecked);
    startTransition(async () => {
      try {
        await promise(newChecked);
        console.log("success");
      } catch {
        setChecked(!newChecked);
        console.log("error");
      }
    });
  };

  return (
    <>
      <input type="checkbox" checked={checked} onChange={handleSubmit} />
      {pending && <span>loading...</span>}
    </>
  );
};

handleSubmitでrequestを行う前にcheckboxのstateを更新し、処理が成功した場合にはstateはそのままに、失敗した場合にはstateは元の状態に戻してあげます。

useOptistimicを使用した例

const UseOptimistic = () => {
  const [checked, setChecked] = useState(false);
  const [pending, startTransition] = useTransition();
  const [optimisticChecked, setOptimisticChecked] = useOptimistic(checked);
  const handleSubmit = () => {
    startTransition(async () => {
      const newChecked = !checked;
      setOptimisticChecked(newChecked);
      try {
        await promise(optimisticChecked);
        setChecked(newChecked);
        console.log("success");
      } catch {
        console.log("error");
      }
    });
  };

  return (
    <>
      <input
        type="checkbox"
        checked={optimisticChecked}
        onChange={handleSubmit}
      />
      {pending && <span>loading...</span>}
    </>
  );
};

handleSubmitでrequest前にsetOptimisticCheckedでstateを更新し、処理が成功すればsetCheckedで元の値の更新を確定させ、処理失敗時には自動的に楽観的更新がリセットされるため何もする必要がありません。optimisticCheckedはリクエストが終了もしくは失敗した時に自動的にcheckedの値に戻ります。

新API:use

useによってpromiseの値を読み取ることができ、promiseが解決されるまでReactはsuspendします。

import { Suspense, use } from "react";

const promise = new Promise<string[]>((resolve) => {
  setTimeout(() => {
    resolve(["comment1", "comment2", "comment3"]);
  }, 1000);
});

const Comments = () => {
  const comments = use(promise);

  return comments.map((comment) => <div key={comment}>{comment}</div>);
};

function Page() {
  return (
    <div>
      <Suspense fallback={<div>loading...</div>}>
        <Comments />
      </Suspense>
    </div>
  );
}

export default Page;

また、useは他のhooksとは違い、以下のように条件分岐の中で呼び出すことも可能です。

const Comments = ({ isNecessary }: { isNecessary: boolean }) => {
  if (!isNecessary) {
    return <div>コメントはありません</div>;
  }

  const comments = use(promise);

  return comments.map((comment) => <div key={comment}>{comment}</div>);
};

function Page() {
  return (
    <div>
      <Suspense fallback={<div>loading...</div>}>
        <Comments isNecessary />
        <Comments isNecessary={false} />
      </Suspense>
    </div>
  );
}

export default Page;

また、以下のようにcontextの値を取得することもできます。

const ThemeContext = createContext({
  color: "red",
});

const Heading = () => {
  const theme = use(ThemeContext);

  return <h1 style={{ color: theme.color }}>Heading</h1>;
};

Discussion