🦋

React 19によって状態管理はどのように変わるのか

2024/07/22に公開

React19のRCが発表され数ヶ月が経ちました。Next.jsではReact19のExperimentalな機能を使った実装をいち早くしていたので、少し馴染みのあるアップデートが多かったように思います。

Next.js(特にApp Router)においてReact19のAPIやHooksをどのように使うべきかはNext.jsのドキュメントを見れば大体のベストプラクティスが見えてきます。ですが、実際の開発現場ではApp Routerを採用しているケース以外にもVite+ReactやPages Router, Remixなどと様々な実装があるのが現実です。

そこでこの記事では、特にVite+Reactのスタックを前提にReact19の新機能をいかに活用できるか整理したいと考えています。

また、React19の新機能を見た時にTanStack QueryやSWRのようなサードパーティの状態管理ライブラリを採用しなくてもアプリケーションが設計できるのではないかという疑問を持ちました。React19のAPIによってどこまでの実装ができ、逆に何が状態管理ライブラリによって補うべきかを考察したいと思います。

はじめに

本記事ではReact19の新機能をVite+Reactを前提にどのように活用すべきかを考察します。そのため、これから紹介する例や解説はすべてクライアントでの実行と捉えていただけると幸いです。

https://ja.react.dev/blog/2024/04/25/react-19

特にピックアップするHooks, APIは以下の3つです。

  • use
  • useActionState
  • useOptimistic

use API

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

useはReactのドキュメントにて以下のように解説がされています。

use はプロミス (Promise) やコンテクストなどのリソースから値を読み取るための React API です。

そして、Promiseを引数にして呼び出した場合にはSupsense・ErrorBoundaryと組み合わせることが紹介されています。これによってReactの標準APIによってSuspenseを用いたリソース取得が可能になります。

useを用いたリソース取得

以下の例はとても簡略的に書いていますがuseを用いることでPromiseからリソースを読み取ることができます。また、そのPromiseがPending中はコンポーネントがサスペンドし、Suspenseのfallbackを表示することができます。

function PostPage() {
  return (
    <ErrorBoundary>
      <Suspense  fallback={<div>Loading...</div>}>
        <Post />
      </Suspense>
    </ErrorBoundary>
  )
}

function Post() {
  const postData = use(fetchPost())

  return <div>{postData}</div>
}

Promiseをキャッシュする必要がある

上記の例を見ると今まで外部のライブラリに頼ってSuspenseのデータ取得を実現していたところが、ReactのAPIによって実装できる点で活用したくなります。しかし、Reactのドキュメントを見てもuseを用いてデータ取得する手法はどこにも紹介されていません。

むしろ、サスペンス対応の外部ライブラリを使用することが推奨されています。

参考:https://ja.react.dev/blog/2024/04/25/react-19#new-feature-use

これは、適切なキャッシュが施されていない場合、レンダリングの度にPromiseが再生成され、必要以上に非同期取得が走ってしまうことに問題があるからです。そのため、本来はuseを使用する際にはPromiseをキャッシュする機構を用意する必要がありますが、自前で実装するにはコストがかかります。

その点においても、いまだTanStack QueryやSWRのような非同期の状態管理ライブラリは活躍すると考えます。

useからコンテクストを読み取る

適切なキャッシュをせずにuseをPromiseからのリソース取得に使用するのは問題があることがわかりました。しかし、useにはもう一つユースケースがあります。

use はプロミス (Promise) やコンテクストなどのリソースから値を読み取るための React API です。

import { use } from 'react';

function Button() {
  const theme = use(ThemeContext);
  // ...

上記のようにuseの引数にコンテキストが渡されることによってuseContextHookと同様の動作がされます。実際クライアントサイドでアプリを構築する際に、こちらのユースケースの方が使用する場面は多いのではないかと感じました。

ではコンテキストからリソースを取得する際に、useを使用する場合とuseContextを使用する場合では何が異なるのでしょうか。

useは条件式の中で呼び出しが可能

大きな違いとして、use条件式の中で呼び出すことが可能です。そのため、以下のような実装が可能になります。

function HorizontalRule({ show }) {
  if (show) {
    const theme = use(ThemeContext);
    return <hr className={theme} />;
  }
  return false;
}

このようにuseが柔軟であることからも、useContextより優先して使用するのが良いとドキュメントで明言されています。

考察

当初useがSuspenseと組み合わせ可能でPromiseからリソースを取得できると知った際に、TanStack Queryなどの状態管理ライブラリを使用せずともSuspenseのデータ取得が可能になるのではないかという疑問が生まれました。

しかしuseを使用する際にはPromiseのキャッシュを施す必要があり、そのキャッシュをTanStack Queryのように柔軟に制御可能にするには相当なコストがかかります。

将来的には、レンダー中にプロミスをキャッシュしやすくする機能を提供する予定です。

RCのブログの中でも、今後レンダー中にプロミスをキャッシュしやすくする機能を提供する予定であるとしていますが、現段階ではSuspenseを用いたデータ取得には状態管理ライブラリを別途採用するのが良いかと思います。

しかし、useのユースケースはそれだけではなく、コンテキストからのリソース取得もありました。さらには、useが条件式の中で使用可能であることからも、非常に柔軟にコンテキストへアクセスができるようになります。

また、ドキュメントにはサーバコンポーネントからクライアントコンポーネントへのデータストリーミングにuseが使用できる例も紹介されていました。今回はクライアントを前提にしているので詳しい解説は控えますが、use自体が幅広く活用できるAPIであることが期待できます。

useActionState Hook

次にuseActionStateHookについて整理します。

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

useActionStateはReactのドキュメントにて以下のように解説がされています。

useActionState は、フォームアクションの結果に基づいてstateを更新するためのフックです。

第一引数にForm送信時に実行するaction関数を、第二引数にstateの初期値として使いたい値をそれぞれ渡します。

公式のexample
import { useActionState } from "react";

async function increment(previousState, formData) {
  return previousState + 1;
}

function StatefulForm() {
  const [state, formAction] = useActionState(increment, 0);
  return (
    <form action={formAction}>
      {state}
      <button>Increment</button>
    </form>
  )
}

外部ライブラリを使用せず状態の更新を管理できる

https://ja.react.dev/blog/2024/04/25/react-19#actions

今まで私たちはFormの状態を効率的に管理するためにReact Hook Formなどのライブラリと組み合わせて開発する機会がありました。それによって「送信中のPending状態」や「エラーハンドリング」をライブラリに任せて実装していました。

React19以前のForm管理

仮にライブラリを採用せずにFormの管理をuseStateで実装すると以下のように煩雑になります。この例では、Formの入力状態・Error状態・Pending状態をそれぞれstateで管理し、Formの実行とともにStateを更新しています。

before
// Before Actions
function UpdateName({}) {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);

  const handleSubmit = async () => {
    setIsPending(true);
    const error = await updateName(name);
    setIsPending(false);
    if (error) {
      setError(error);
      return;
    } 
    redirect("/path");
  };

  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

このように煩雑なForm管理を効率的に実装できるのがReact Hook FormのようなFormライブラリのメリットであり、useStateのようなcontrollableなState管理ではなくuncontrollableにFormを管理する機会が多かったように思います。

React Hook Formを使った実装
import { useForm, SubmitHandler } from "react-hook-form"

type Inputs = {
  example: string
  exampleRequired: string
}

export default function App() {
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors },
  } = useForm<Inputs>()
  const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input defaultValue="test" {...register("example")} />
      <input {...register("exampleRequired", { required: true })} />
      {errors.exampleRequired && <span>This field is required</span>}
      <input type="submit" />
    </form>
  )
}

React19以後のForm管理

このように外部ライブラリを使用しないと煩雑化していたForm管理ですが、React19によって追加・拡張されたuseActionState<form>によってReactの機能だけで効率的にForm管理が可能になります。

まず、React19からformコンポーネントのactionが拡張され、関数を渡すことが可能になります。action にURLが渡された場合は、フォームはHTMLのformコンポーネントと同様に動作します。

https://ja.react.dev/reference/react-dom/components/form

そしてactionに渡された関数は送信されたフォームのFormDataを引数として呼び出すことができます。これによって、よりシンプルに更新するFormDataへアクセスが可能になります。

formDataへアクセスする例
export default function Search() {
  function search(formData) {
    const query = formData.get("query");
  }
  return (
    <form action={search}>
      <input name="query" />
      <button type="submit">Search</button>
    </form>
  );
}

そしてuseActionStateと組み合わせることでよりFormの管理がシンプルになります。下の例では、useActionStateの第一引数にformDataを受け取って状態を更新する関数を渡しています。

この関数の中ではデータの更新だけでなく、エラーハンドリングも行っており発生したエラーに対してはuseActionStateの返り値からアクセスすることができます。

さらに返却される3つ目の値としてisPendingのフラグを扱うことができます。これは、actionが実行されている間trueになるため、Form送信中にボタンをdisabledにするなどの制御に便利です。

useActionStateと組み合わせる
function ChangeName({ name, setName }) {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await updateName(formData.get("name"));
      if (error) {
        return error;
      }
      redirect("/path");
      return null;
    },
    null,
  );

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

考察

本節でも比較したように、React19前後ではFormの管理が非常に効率的になっているのが分かります。

今までは単純なFormの管理であっても、送信するデータやエラー・Pendingの状態をStateで実装するには煩雑になることから、少しtoo muchと感じながらもライブラリを選定するケースが多々ありました。

しかしReact19からFormの管理が効率的になることにより、まずはReactの機能の中でFormを設計し、足りない要素をライブラリで補うという発想に転換できるのではないかと考えています。

もちろん、既存のFormライブラリの存在は未だ必要です。例えば、複雑でネストしたFormを実装する際には標準の機能だけでは管理が大変なケースは出てくるでしょう。

ですがuseActionStateを活用してFormを実装することは、次に紹介する楽観的更新も含めてとても便利だと感じます。

useOptimistic Hook

個人的に最も嬉しい機能追加がこのuseOptimisticです。

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

useOptimisticはReactのドキュメントにて以下のように解説がされています。

useOptimistic は、UIを楽観的に (optimistically) 更新するためのReactフックです。

楽観的更新とは、非同期リクエストの進行中に最終的に得るはずの状態を先に楽観的に表示しておくというものです。例えばTwitter(X)のいいねなどがよく例にあがりますが、特にデータ更新から描画までのインタラクションがUXに直結するケースにおいて重宝されます。

以前までの楽観的更新の実装

React19以前において、楽観的更新を実装するには少し複雑な処理が必要でした。ここではTanStack Queryでの楽観的更新の実装を例に挙げます。

楽観的更新を実装をするための流れは以下3つの段階に分けられます。

  1. データの更新を開始する
  2. 更新が成功した場合に描画するデータをあらかじめ反映させる
  3. 更新に成功した場合は改めてデータを再取得する。更新に失敗した場合はFallbackする。

https://tanstack.com/query/v5/docs/framework/react/guides/optimistic-updates#updating-a-list-of-todos-when-adding-a-new-todo

TanStack Queryを使用した例
const queryClient = useQueryClient()

useMutation({
  mutationFn: updateTodo,
  // When mutate is called:
  onMutate: async (newTodo) => {
    // Cancel any outgoing refetches
    // (so they don't overwrite our optimistic update)
    await queryClient.cancelQueries({ queryKey: ['todos'] })

    // Snapshot the previous value
    const previousTodos = queryClient.getQueryData(['todos'])

    // Optimistically update to the new value
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

    // Return a context object with the snapshotted value
    return { previousTodos }
  },
  // If the mutation fails,
  // use the context returned from onMutate to roll back
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previousTodos)
  },
  // Always refetch after error or success:
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})

こちらのコードを見ていかがでしょうか。楽観的更新で実行したい処理以上にTanStack Queryのコードは複雑なことをしているように見えます。

これはTanStack Query固有のキャッシュの扱いやコールバック関数の実行順序を把握しておかなくてはいけない点が背景にあると考えています。

TanStack Queryのキャッシュを柔軟に管理できる特徴は便利ですが、楽観的更新に関してはコードの可読性や実装の理解がTanStack Queryへの慣れに依存してしまう問題点があると感じていました。

React19による楽観的更新の実装

このような楽観的更新の複雑さをuseOptimisticが解決していると考えています。最もシンプルな使用方法としては以下の通りです。

シンプルな使用法
function ChangeName({currentName, onUpdateName}) {
  const [optimisticName, setOptimisticName] = useOptimistic(currentName);

  const submitAction = async formData => {
    const newName = formData.get("name");
    setOptimisticName(newName);
    const updatedName = await updateName(newName);
    onUpdateName(updatedName);
  };

  return (
    <form action={submitAction}>
      <p>Your name is: {optimisticName}</p>
      <p>
        <label>Change Name:</label>
        <input
          type="text"
          name="name"
          disabled={currentName !== optimisticName}
        />
      </p>
    </form>
  );
}

useOptimistic第一引数に初期値を設定し、楽観的更新が反映されるStateと楽観的更新を実行するためのディスパッチ関数が返却されます。

これをアクション関数内のAPIリクエストを呼び出す前に実行するだけで楽観的更新を実装することができます。

考察

React19以前でもTanStack Queryなどで楽観的更新を実現することは可能でした。しかし、その場合はライブラリ固有のキャッシュやAPIに対する理解が不可欠であり、一見処理が複雑に見えていました。

そこでReact19のuseOptimisticを使用するとライブラリ固有の理解がなくても楽観的更新を実装できるようになります。また、前節で紹介したuseActionStateと組み合わせることで同時にFormの状態も管理することができます。

これだけのことがReactのHooksによって実現できるというのは非常に便利だなと感じます。

まとめ

今回はReact19で新たに追加される機能をuseuseActionStateuseOptimisticの3つに絞って解説しました。またReact19以前と比較をすると、状態の更新(Action)に関する管理が特に改善していることが分かります。

それにより、今まではライブラリに頼って実装していたところがReactが持つHooksによって解決できるケースが増えると思います。

もちろん外部ライブラリに頼れば今まで通り実装をすることは可能です。しかし、外部ライブラリに依存せずにReactの標準機能を使って実装することは、チーム間での学習コストの低下やバンドルサイズの削減などさまざまな観点からもメリットがあると考えます。

その点においてもまずはReactのHooksで実装を検討し、足りない機能を補うように薄くライブラリに頼るという考え方をより一層意識していきたいと感じました。

最後に

AI Shiftではエンジニアの採用に力を入れています!
少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか?
(オンライン・19時以降の面談も可能です!)

【面談フォームはこちら】

https://hrmos.co/pages/cyberagent-group/jobs/1826557091831955459

AI Shift Tech Blog

Discussion