⚛︎

React v19からローディング状態を管理しなくてよくなる

2024/02/19に公開

TL;DR

  • v17までは非同期処理の処理状態を管理する必要があった
  • v18では読み取り処理状態を管理する必要がなくなった(Suspense)
  • v19では書き込み処理状態を管理する必要がなくなった(form)

はじめに

サーバーと通信するなど非同期処理を行うとき、処理状態をUIに反映するために状態の管理が必要でした。v19から管理する必要がなくなってスッキリ書けます。(React v19はまだリリースされていません。ブログ

読み取り処理状態の扱いの比較

v17までのReactは、処理状態を定義し、処理状況を反映するコードを書く必要がありました。

😢 ページを開いた時にデータを取得して表示する例(v17):loadingを管理している

function Component() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    getAsynchronously().then((res) => {
      setData(res.data);
      setLoading(false);
    });
  }, []);

  return (
    <div>
      {loading && <p>loading...</p>}
      {data && <p>{data}</p>}
    </div>
  );
}

v18ではSuspenseが登場しました。
非同期の読み取り処理の処理状態について管理する必要がなくなりました。

😀 ページを開いた時にデータを取得して表示する例(v18):loadingを管理する必要なし

function Component() {
  const getPromise = getAsynchronously();
  return (
    <div>
      <Suspense fallback={<p>loading...</p>}>
        <Data getPromise={getPromise} />
      </Suspense>
    </div>
  );
}

// useはexperimentalであり、React公式により同様の機能を持ったサードパーティライブラリの利用が推奨されている。
// https://ja.react.dev/reference/react/Suspense#usage 補足参照
function Data({ getPromise }) {
  const { data } = use(getPromise);
  return <p>{data}</p>;
}

書き込み処理状態の扱いの比較

書き込み時も同様に、処理状態を定義し、処理状況を反映するコードを書く必要がありました。

😢 ボタンをクリックした時にデータを送信する例(v18):pendingを管理している

function Component() {
  const [pending, setPending] = useState(false);

  const post = () => {
    setPending(true);
    postAsynchronously().then(() => {
      setPending(false);
    });
  };

  return <button onClick={post}>{pending ? "sending..." : "post"}</button>;
}

formが登場しました。Suspenseのような新機能の追加ではなく、既存のformを少し改良したようです。

😀 ボタンをクリックした時にデータを送信する例(v19):pendingを管理する必要なし

function Component() {
  const post = async () => {
    await postAsynchronously();
  };

  return (
    <form action={post}>
      <Button />
    </form>
  );
}

function Button() {
  const { pending } = useFormStatus();
  return <button>{pending ? "sending..." : "post"}</button>;
}

非同期の書き込み処理の処理状態について管理する必要がなくなりました。
代わりに、form内のコンポーネントにてuseFormStateで書き込みの処理状態を取得し、書き込み中のUIと書き込み後のUIが宣言されるのみとなりました。
(Tips: 楽観的更新と呼ばれるUX向上手法を取り入れている場合も自分で処理状態を管理しなくて良いように専用hookのuseOptimisticが登場していて最高です。)

おわりに

非同期的な読み書きもスッキリ書けるようになって最高です。
デモ置いておきます。

DEMO
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React canary</title>
    <script type="importmap">
      {
        "imports": {
          "@jsxImportSource": "https://esm.sh/react@18.3.0-canary-a9cc32511-20240215",
          "react": "https://esm.sh/react@18.3.0-canary-a9cc32511-20240215",
          "react-dom": "https://esm.sh/react-dom@18.3.0-canary-a9cc32511-20240215",
          "react-dom/client": "https://esm.sh/react-dom@18.3.0-canary-a9cc32511-20240215/client"
        }
      }
    </script>
    <script type="module" src="https://esm.sh/run" defer></script>
  </head>

  <body>
    <div id="root"></div>
    <script type="text/babel">
      import { createRoot } from "react-dom/client";
      import { use, Suspense, useState, useEffect, useRef } from "react";
      import { useFormStatus } from "react-dom";

      function React17Get() {
        const [data, setData] = useState(null);
        const [loading, setLoading] = useState(false);

        useEffect(() => {
          setLoading(true);
          getAsynchronously().then((res) => {
            setData(res.data);
            setLoading(false);
          });
        }, []);

        return (
          <div>
            {loading && <p>loading...</p>}
            {data && <p>{data}</p>}
          </div>
        );
      }

      function React18() {
        const getPromise = getAsynchronously();
        return (
          <div>
            <Suspense fallback={<p>loading...</p>}>
              <Data getPromise={getPromise} />
            </Suspense>
          </div>
        );
      }

      function Data({ getPromise }) {
        const { data } = use(getPromise);
        return <p>{data}</p>;
      }

      function React17Post() {
        const [pending, setPending] = useState(false);

        const post = () => {
          setPending(true);
          postAsynchronously().then(() => {
            setPending(false);
          });
        };

        return (
          <button onClick={post}>{pending ? "sending..." : "post"}</button>
        );
      }

      function React19() {
        const post = async () => {
          await postAsynchronously();
        };
        return (
          <form action={post}>
            <Button />
          </form>
        );
      }

      function Button() {
        const { pending } = useFormStatus();
        return <button>{pending ? "sending..." : "post"}</button>;
      }

      function App() {
        return (
          <>
            <React17Get />
            <React18 />
            <React17Post />
            <React19 />
          </>
        );
      }

      const root = createRoot(document.querySelector("#root"));
      root.render(<App />);

      async function getAsynchronously() {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        return { data: "hello world" };
      }

      async function postAsynchronously() {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        return { data: "hello world" };
      }
    </script>
  </body>
</html>

Discussion