React 19によって状態管理はどのように変わるのか
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を前提にどのように活用すべきかを考察します。そのため、これから紹介する例や解説はすべてクライアントでの実行と捉えていただけると幸いです。
特にピックアップするHooks, APIは以下の3つです。
- use
- useActionState
- useOptimistic
use
API
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
の引数にコンテキストが渡されることによってuseContext
Hookと同様の動作がされます。実際クライアントサイドでアプリを構築する際に、こちらのユースケースの方が使用する場面は多いのではないかと感じました。
ではコンテキストからリソースを取得する際に、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
次にuseActionState
Hookについて整理します。
useActionState
はReactのドキュメントにて以下のように解説がされています。
useActionState は、フォームアクションの結果に基づいてstateを更新するためのフックです。
第一引数にForm送信時に実行するaction関数を、第二引数にstateの初期値として使いたい値をそれぞれ渡します。
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>
)
}
外部ライブラリを使用せず状態の更新を管理できる
今まで私たちはFormの状態を効率的に管理するためにReact Hook Formなどのライブラリと組み合わせて開発する機会がありました。それによって「送信中のPending状態」や「エラーハンドリング」をライブラリに任せて実装していました。
React19以前のForm管理
仮にライブラリを採用せずにFormの管理をuseStateで実装すると以下のように煩雑になります。この例では、Formの入力状態・Error状態・Pending状態をそれぞれstateで管理し、Formの実行とともにStateを更新しています。
// 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コンポーネントと同様に動作します。
そしてactionに渡された関数は送信されたフォームの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にするなどの制御に便利です。
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
です。
useOptimistic
はReactのドキュメントにて以下のように解説がされています。
useOptimistic は、UIを楽観的に (optimistically) 更新するためのReactフックです。
楽観的更新とは、非同期リクエストの進行中に最終的に得るはずの状態を先に楽観的に表示しておくというものです。例えばTwitter(X)のいいねなどがよく例にあがりますが、特にデータ更新から描画までのインタラクションがUXに直結するケースにおいて重宝されます。
以前までの楽観的更新の実装
React19以前において、楽観的更新を実装するには少し複雑な処理が必要でした。ここではTanStack Queryでの楽観的更新の実装を例に挙げます。
楽観的更新を実装をするための流れは以下3つの段階に分けられます。
- データの更新を開始する
- 更新が成功した場合に描画するデータをあらかじめ反映させる
- 更新に成功した場合は改めてデータを再取得する。更新に失敗した場合はFallbackする。
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で新たに追加される機能をuse
、useActionState
、useOptimistic
の3つに絞って解説しました。またReact19以前と比較をすると、状態の更新(Action)に関する管理が特に改善していることが分かります。
それにより、今まではライブラリに頼って実装していたところがReactが持つHooksによって解決できるケースが増えると思います。
もちろん外部ライブラリに頼れば今まで通り実装をすることは可能です。しかし、外部ライブラリに依存せずにReactの標準機能を使って実装することは、チーム間での学習コストの低下やバンドルサイズの削減などさまざまな観点からもメリットがあると考えます。
その点においてもまずはReactのHooksで実装を検討し、足りない機能を補うように薄くライブラリに頼るという考え方をより一層意識していきたいと感じました。
最後に
AI Shiftではエンジニアの採用に力を入れています!
少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか?
(オンライン・19時以降の面談も可能です!)
【面談フォームはこちら】
Discussion