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

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

React 19では、新しいhooksである`useOptimistic`が導入されました。このhooksを使用すると、**サーバーからの応答を待たずに、UIを楽観的に更新することができます**。本記事では、`useOptimistic`の機能や使い方、特に`<form action={asyncHandler}>`と組み合わせた際の活用方法について詳しく解説します。また、理解を深めるために、`useOptimistic`の内部実装である`updateReducerImpl`周辺のコードを通してその動作原理を探っていきます。

## デモ

https://codesandbox.io/embed/hfp7ss?view=preview&module=%2Findex.js

## `useOptimistic`の使い方

`useOptimistic`は、以下のように使用します。

```jsx
const [optimisticState, addOptimisticUpdate] = useOptimistic(initialState, reducer);
```

- `initialState`: 楽観的な更新の基準となる初期状態。
- `reducer`（オプション）: 楽観的な状態更新を制御するための`reducer`関数。

### フォームの`action`属性と楽観的更新の使用例

React 19では、フォームの`action`属性に非同期関数を指定して、フォーム送信時に非同期処理をトリガーできます。`useOptimistic`と組み合わせることで、非同期操作が完了する前にUIを楽観的に更新できます。

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

[ソース](https://github.com/facebook/react/blob/c11c9510fa14bbd87053685c19bfdfec2f427f49/packages/react-reconciler/src/ReactFiberHooks.js#L2038)

```javascript: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`とは異なり、レンダリング中の更新をサポートしていません。

### `useOptimistic`と`useState`の違い

- **レンダーフェーズでの更新:** `useOptimistic`はレンダーフェーズでの更新をサポートしていません。一方、`useState`は可能です。
- **パススルー値の扱い:** `useOptimistic`は、`passthrough`が更新されると楽観的な状態がリセットまたは再計算されますが、`useState`ではこのような挙動はありません。

## 重要な考慮点

### 1. フォームの`action`属性と非同期処理

React 19では、フォームの`action`属性に関数を指定して、フォーム送信時に非同期処理を行うことができます。

**例:**

```jsx
<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](https://react.dev/reference/react/useOptimistic)
- React公式ドキュメント: [フォームの`action`属性](https://react.dev/reference/react-dom/components/form)
- React公式ドキュメント: [useTransition](https://react.dev/reference/react/useTransition)
- Reactのソースコード: [ReactFiberHooks.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js)
- Reactの変更履歴: [CHANGELOG.md](https://github.com/facebook/react/blob/main/CHANGELOG.md)