8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React19: useOptimistic を用いた楽観的UI更新と内部実装の解説

Last updated at Posted at 2024-12-10

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を楽観的に更新できます。

ContactForm.js
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はこの関数を利用して楽観的な状態管理を実現しています。

ソース

packages/react-reconciler/src/ReactFiberHooks.js
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とは異なり、レンダリング中の更新をサポートしていません。

useOptimisticuseStateの違い

  • レンダーフェーズでの更新: 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属性と組み合わせて使用する事が多そうです。

  • useTransitionstartTransitionを使用することで、非同期な状態更新をUIの応答性を損なわずに処理できます。これらをuseOptimisticと併用することで、よりスムーズなユーザーエクスペリエンスを提供できます。

  • 楽観的な状態の再計算: useOptimisticの第一引数であるpassthroughが更新されると、楽観的な状態がリセットされます。ただし、第2引数にreducer関数を渡すことで、新しいpassthroughに基づいて楽観的な状態を再計算できます。**

  • 連続した状態更新への対処: 他のhooksと同様に、ボタンの連打などの高速な状態更新に対しても、適切に実装しないと不整合が生じる可能性があります。**

  • エラーハンドリングの実装: useOptimistic自体はエラー処理を提供しないため、非同期操作が失敗した場合の対策を自分で実装する必要があります。最近ではErrorBoundaryを活用することが多いです。**

参考リンク

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?