React19: useOptimistic を用いた楽観的UI更新と内部実装の解説
React 19では、新しいhooksであるuseOptimistic
が導入されました。このhooksを使用すると、サーバーからの応答を待たずに、UIを楽観的に更新することができます。本記事では、useOptimistic
の機能や使い方、特に<form action={asyncHandler}>
と組み合わせた際の活用方法について詳しく解説します。また、理解を深めるために、useOptimistic
の内部実装であるupdateReducerImpl
周辺のコードを通してその動作原理を探っていきます。
デモ
useOptimistic
の使い方
useOptimistic
は、以下のように使用します。
const [optimisticState, addOptimisticUpdate] = useOptimistic(initialState, reducer);
-
initialState
: 楽観的な更新の基準となる初期状態。 -
reducer
(オプション): 楽観的な状態更新を制御するためのreducer
関数。
フォームのaction
属性と楽観的更新の使用例
React 19では、フォームのaction
属性に非同期関数を指定して、フォーム送信時に非同期処理をトリガーできます。useOptimistic
と組み合わせることで、非同期操作が完了する前にUIを楽観的に更新できます。
import React, { useOptimistic } from 'react';
function ContactForm() {
const [optimisticData, addOptimisticData] = useOptimistic(
{ name: '', email: '', message: '' },
(prevData, newData) => ({ ...prevData, ...newData })
);
async function handleSubmit(formData) {
// 楽観的な更新を適用
addOptimisticData({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
});
// 非同期操作(例:サーバーへのデータ送信)
try {
const response = await fetch('/api/contact', {
method: 'POST',
body: formData,
});
const result = await response.json();
// 必要に応じて最終的な状態に更新
} catch (error) {
console.error('送信エラー:', error);
// 必要であれば楽観的な更新を元に戻す
}
}
return (
<form action={handleSubmit}>
<input name="name" placeholder="名前" defaultValue={optimisticData.name} />
<input name="email" placeholder="メールアドレス" defaultValue={optimisticData.email} />
<textarea name="message" placeholder="メッセージ" defaultValue={optimisticData.message} />
<button type="submit">送信</button>
</form>
);
}
export default ContactForm;
ポイント:
- フォームの
action
属性に非同期関数handleSubmit
を指定します。これにより、フォーム送信時にhandleSubmit
が呼び出されます。 -
addOptimisticData
を使用して、非同期操作が完了する前に楽観的な状態更新を行います。 - 非同期操作が成功した場合は、必要に応じて状態を更新します。失敗した場合は、エラーハンドリングを行います。
内部実装とupdateReducerImpl
の役割
useOptimistic
は、内部的にはuseReducer
に似た仕組みで実装されていますが、いくつか重要な違いがあります。特に、updateReducerImpl
関数が状態の更新と再計算において重要な役割を果たしています。
updateReducerImpl
の動作
updateReducerImpl
は、hooksの状態を更新するための内部関数であり、useOptimistic
はこの関数を利用して楽観的な状態管理を実現しています。
function updateOptimisticImpl<S, A>(
hook: Hook,
current: Hook | null,
passthrough: S,
reducer: ?(S, A) => S,
): [S, (A) => void] {
// passthrough値に基づいてベース状態を更新
hook.baseState = passthrough;
// reducer関数を決定
const resolvedReducer: (S, A) => S =
typeof reducer === 'function' ? reducer : basicStateReducer;
// 保留中の更新を適用して状態を再計算
return updateReducerImpl(hook, current, resolvedReducer);
}
ポイント:
-
passthrough
(第一引数)が更新されると、保留中の更新を再適用して状態を再計算します。 -
reducer
関数を指定することで、新しいpassthrough
に基づいた楽観的な状態を再計算できます。 -
useState
とは異なり、レンダリング中の更新をサポートしていません。
useOptimistic
とuseState
の違い
-
レンダーフェーズでの更新:
useOptimistic
はレンダーフェーズでの更新をサポートしていません。一方、useState
は可能です。 -
パススルー値の扱い:
useOptimistic
は、passthrough
が更新されると楽観的な状態がリセットまたは再計算されますが、useState
ではこのような挙動はありません。
重要な考慮点
1. フォームのaction
属性と非同期処理
React 19では、フォームのaction
属性に関数を指定して、フォーム送信時に非同期処理を行うことができます。
例:
<form action={handleSubmit}>
{/* フォームフィールド */}
</form>
- この機能は、サーバーコンポーネントや
React.startTransition
と組み合わせて使用されることが多いです。
2. useOptimistic
とフォームの組み合わせ
useOptimistic
を使用して、非同期処理が完了する前に楽観的な状態更新を行う際には、以下の点に注意してください。
- データの整合性: 楽観的な更新によって表示されるデータが、サーバー側の処理結果と一致しない可能性があります。
-
エラーハンドリング: 非同期操作が失敗した場合、楽観的な状態を元に戻す、またはユーザーに適切なエラーメッセージを表示する、
ErrorBoundary
を設置するなどの適切な処置をする必要があります。
3. 複数回の連続したユーザーアクションへの対処
他のhooks同様に、ボタンの連打やフォームの連続送信など、短時間で複数回addOptimisticData
が呼び出される可能性がある場合、状態管理を適切に行わないと不整合が生じる可能性があります。
対策例:
- 送信ボタンを一時的に無効化する。
- ローディング状態を表示し、再送信を防ぐ。
- 非同期操作が完了するまで次の送信を待機する。
まとめ
useOptimistic
は、フォームのaction
属性と組み合わせることで、非同期操作が完了する前にUIを楽観的に更新し、ユーザーエクスペリエンスを向上させる強力なツールです。その動作を正しく理解し、適切なエラーハンドリングや状態管理を行うことが重要です。
キーとなるポイント:
-
useOptimistic
を使用することで、非同期操作が完了する前にUIを楽観的に更新できます。フォームのaction
属性と組み合わせて使用する事が多そうです。 -
useTransition
やstartTransition
を使用することで、非同期な状態更新をUIの応答性を損なわずに処理できます。これらをuseOptimistic
と併用することで、よりスムーズなユーザーエクスペリエンスを提供できます。 -
楽観的な状態の再計算:
useOptimistic
の第一引数であるpassthrough
が更新されると、楽観的な状態がリセットされます。ただし、第2引数にreducer
関数を渡すことで、新しいpassthrough
に基づいて楽観的な状態を再計算できます。** -
連続した状態更新への対処: 他のhooksと同様に、ボタンの連打などの高速な状態更新に対しても、適切に実装しないと不整合が生じる可能性があります。**
-
エラーハンドリングの実装:
useOptimistic
自体はエラー処理を提供しないため、非同期操作が失敗した場合の対策を自分で実装する必要があります。最近ではErrorBoundary
を活用することが多いです。**
参考リンク
- React公式ドキュメント: useOptimistic
- React公式ドキュメント: フォームの
action
属性 - React公式ドキュメント: useTransition
- Reactのソースコード: ReactFiberHooks.js
- Reactの変更履歴: CHANGELOG.md