Concurrent Mode時代のReact設計論 (4) コンポーネント設計にサスペンドを組み込む

この記事は「Concurrent Mode時代のReact設計論」シリーズの4番目の記事です。

## シリーズ一覧
- [Concurrent Mode時代のReact設計論 (1) Concurrent Modeにおける非同期処理](https://qiita.com/uhyo/items/4a6315bfccf387407631)
- [Concurrent Mode時代のReact設計論 (2) useTransitionを活用する](https://qiita.com/uhyo/items/ba49b25f0a206e933e4d)
- [Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか](https://qiita.com/uhyo/items/27c1282addc06b17a980)
- **Concurrent Mode時代のReact設計論 (4) コンポーネント設計にサスペンドを組み込む**
- [Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する](https://qiita.com/uhyo/items/52ab8e2cab30b0811294)
- [Concurrent Mode時代のReact設計論 (6) Concurrent Modeと副作用](https://qiita.com/uhyo/items/fa443351fd4444dacaf7)
- Concurrent Mode時代のReact設計論 (7) ステート管理ライブラリの展望（仮）
- Concurrent Mode時代のReact設計論 (8) まとめ（仮）

## コンポーネント設計にサスペンドを組み込む

前回の最後にrender-as-you-fetchという概念が出てきました。これは、ReactのConcurrent Modeのドキュメントにおいて提唱されているUXパターンであり、読み込んで表示すべきデータが複数ある場合に、全てが読み込み完了するまで待つのではなく読み込めたデータから順に表示するというものです。

このパターンの良し悪しはともかく、これはConcurrent Mode時代のコンポーネント設計を議論するための格好の題材です。

### 基本パターン: データごとにPromiseを分ける

Concurrent Modeにおいてrender-as-you-fetchを実現するには、それぞれのデータに対して異なるPromise（`Fetcher`）を用意する必要があります。そして、各データを担当するコンポーネントを用意して、それぞれのコンポーネントがサスペンドします。

そうすることで、それぞれのデータが用意できた段階でコンポーネントのサスペンドが解除（再レンダリング）され、その部分のデータが表示されます。

具体例として、ユーザーのリストを3種類読み込んでrender-as-you-fetch戦略で表示するコンポーネントを書いてみるとこんな感じです。

```tsx
const PageB: FunctionComponent<{
  dailyRankingFetcher: Fetcher<User[]>;
  weeklyRankingFetcher: Fetcher<User[]>;
  monthlyRankingFetcher: Fetcher<User[]>;
}> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => {
  return (
    <>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={dailyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={weeklyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={monthlyRankingFetcher} />
      </Suspense>
    </>
  );
};

const Users: FunctionComponent<{
  usersFetcher: Fetcher<User[]>;
}> = ({ usersFetcher }) => {
  const users = usersFetcher.get();
  return (
    <ul>
      {users.map(({ id, name }) => (
        <li key={id}>{name}</li>
      ))}
    </ul>
  );
};
```

`PageB`は3種類の`Fetcher<User[]>`を受け取ります。実際に`Fetcher<User[]>`から得てデータを表示するのは別に用意した`Users`コンポーネントが担当しており、`PageB`の役割は各`Users`要素を`Suspense`で囲むことです。

ポイントは、このように`Users`をそれぞれ`Suspense`で囲まないといけないということです。復習すると、`Suspense`の役割はその内部で発生したサスペンドをキャッチして、その場合に`fallback`で指定されたフォールバックコンテンツを代わりにレンダリングすることです。`Suspense`の中のどこでサスペンドが発生しようと、その`Suspense`の中身全体がフォールバックします。

このことから、`Suspense`を用いてrender-as-you-fetchパターンを実装するには、あるコンポーネントがサスペンドしても他の部分に影響を与えないようにする必要があります。ここでは`Suspense`を複数並べることでこれを達成しています。

実際この`PageB`を適当なデータでレンダリングすると、下のスクリーンショットのように一つずつ`Loading users...`が`Users`によってレンダリングされたデータに置き換わっていく挙動をとります。

![screenshot4-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233667/c6812437-cfa2-ce5d-d488-030267c7f28f.png)

ちなみに、`Suspense`の組み立て方によって色々な表示パターンを実現することができます。例えば、次のようにすると、`dailyRankingFetcher`が用意できるまでは何も表示せず、用意できたら残りを待つという挙動になります。

```tsx
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={dailyRankingFetcher} />
        <Suspense fallback={<p>Loading users...</p>}>
          <Users usersFetcher={weeklyRankingFetcher} />
          <Suspense fallback={<p>Loading users...</p>}>
            <Users usersFetcher={monthlyRankingFetcher} />
          </Suspense>
        </Suspense>
      </Suspense>
```

このように、コンポーネントが非同期処理の結果をどのように待ってどう表示するのかというロジックは`Suspense`を用いて書くことができます。これはつまり、Concurrent Modeではrender-as-you-fetchパターンに必要なロジックを実際にそのデータを表示するコンポーネントが（内部でのコンポーネント分割は起こりますが）記述できるということを表しています。前回の記事で示した問題の一つがConcurrent Modeでは解決されているわけです。

おまけに、`Suspense`という道具を用いて、データローディングに係るロジックをJSXというきわめて宣言的な表現により記述することができています。一応誤解がないように述べておくとJSXという構文は重要ではなく、本質的にこの点に寄与しているのはコンポーネントが成す木構造という表現方法なのですが。

なお、この立場に立つと、レンダリングのサスペンドというのはコンポーネントが発生させる現象ですから、サスペンドする役割を持つコンポーネント（今回は`Users`）を明確にすることが重要になります。コンポーネントにdoc commentなどを書く際に、「このコンポーネントはいつサスペンドするのか」を明示しておくのもよいでしょう。

### `useTransition`と`Suspense`の関係

まず`useTransition`について復習します。このフックからは`startTransition`関数を得ることができ、`startTransition`の内部で発生したステート更新により再レンダリングが発生してそのレンダリングでサスペンドが発生した場合、サスペンドが解消されるまで画面に更新前のステートを表示し続けられるというものでした。

`useTransition`が絡むと、`Suspense`に係るコンポーネント設計はかなり複雑な様相をとります。これに関連して、ひとつ重要な事実を覚えていただく必要があります。

それは、**再レンダリング時に新たにマウントされた`Suspense`の中で起きたサスペンドは`useTransition`からは無視される**という点です。言い方を変えれば、**`useTransition`の効果を発動するには、あらかじめ用意してあった`Suspence`にサスペンドをキャッチしてもらう必要がある**ということです。

この挙動はバグなのではと筆者は一瞬思いましたが、[このissue](https://github.com/facebook/react/issues/17401)で説明されている通りこれは仕様です。

コンポーネントを設計する際にはこのことを念頭に考える必要があります。すなわち、あるコンポーネントの中でサスペンドを発生させるにあたり、それが`useTransition`に対応するサスペンド（外部の`Suspense`によりキャッチされることを意図したサスペンド）なのか、それとも`useTransition`に対応しないサスペンド（自身の中で新たに生成した`Suspense`によりキャッチされるサスペンド）なのかを意識的に区別しなければならないということです。

では、先ほど出てきた`PageB`の場合はどうでしょうか。

```tsx
const PageB: FunctionComponent<{
  dailyRankingFetcher: Fetcher<User[]>;
  weeklyRankingFetcher: Fetcher<User[]>;
  monthlyRankingFetcher: Fetcher<User[]>;
}> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => {
  return (
    <>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={dailyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={weeklyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={monthlyRankingFetcher} />
      </Suspense>
    </>
  );
};
```

`startTransition`中のステート更新で新たに`PageB`コンポーネントがマウントされた場合、3つの`Suspense`コンポーネントがマウントされ、その中で発生したサスペンドは即座にキャッチされます。このとき、ステート更新によって発生したサスペンドは全て新たにマウントされた`Suspense`によってキャッチされることになります。

よって、このステート更新では`useTransition`の効果は発揮されません。ステート更新が行われた瞬間に`PageB`がレンダリングされDOMに反映されます。`PageB`は最初3つの`Loading users...`を表示することになるでしょう。実際、この`PageB`に前回の記事で出てきた`Root`と`PageA`を繋げてみるとそのような挙動になります。興味がある方は実際にやってみましょう。

### `useTransition`のための応用的なコンポーネントデザイン

このことを踏まえて、`PageB`を`useTransition`に対応するように改良するにはどうすればよいか考えてみましょう。もし「全部ロードされるまで前の画面を表示し続けたい」という場合は話は簡単で、それぞれの`Users`コンポーネントを`Suspense`で囲むのをやめればよいです。また、例えば「`dailyRankingFetcher`がロード完了するまでは前の画面を表示し続けたい」のような場合も、対応する`Users`だけ`Suspense`で囲まなければ対応できます。

厄介なのは、「どれか1つのデータが読み込めるまでは前の画面を表示し続けたい」というような場合です。この場合はただ`Suspense`を消すだけでは達成できません。

Promiseの機能を思い出すと、「どれか1つのPromiseが解決するまで待つ」という挙動は`Promise.race`により達成できます。ということで、今回は`Fetcher.race`を用意すれば解決できますね。

`Fetcher.race`の実装を用意するとこんな感じです（コンストラクタをあのインターフェースにしたので実装がひどいことになっていますがサンプルだと思って大目に見てください）。

```tsx
  static race<T extends Fetcher<any>[]>(
    fetchers: T
  ): Fetcher<FetcherValue<T[number]>> {
    for (const f of fetchers) {
      if (f.state.state === "fulfilled") {
        const result = new Fetcher<any>(() => Promise.resolve());
        result.state = {
          state: "fulfilled",
          value: f.state.value
        };
        return result;
      } else if (f.state.state === "rejected") {
        const result = new Fetcher<any>(() => Promise.resolve());
        result.state = {
          state: "rejected",
          error: f.state.error
        };
      }
    }
    return new Fetcher(() =>
      Promise.race(fetchers.map(f => (f as any).promise))
    );
  }
```

ちなみに、型に出てきた`FetcherValue`はこのように定義しています。型安全な実装が厳しい場合でも、型パズルでも何でも駆使して関数のインターフェースだけは正確さを守るというのが堅牢なTypeScriptプログラムを書くコツです。

```tsx
type FetcherValue<F> = F extends Fetcher<infer T> ? T : unknown;
```

話を元に戻すと、この`Fetcher.race`を使って`PageB`をこのように定義すれば、「どれか1つのデータが来るまでサスペンドする」という挙動が実現できます。`Fetcher.race([...])`は`get`メソッドを使用するためだけに作られており、その値は`PageB`直下では使われていません。このように、値を得ることではなくサスペンドすることが主目的の`Fetcher`というのも存在し得ます。少し話が違いますが、筆者も第1回の記事で紹介したアプリケーションでは`Fetcher<void>`を多用しています。

```tsx
const PageB: FunctionComponent<{
  dailyRankingFetcher: Fetcher<User[]>;
  weeklyRankingFetcher: Fetcher<User[]>;
  monthlyRankingFetcher: Fetcher<User[]>;
}> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => {
  Fetcher.race([
    dailyRankingFetcher,
    weeklyRankingFetcher,
    monthlyRankingFetcher
  ]).get();

  return (
    <>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={dailyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={weeklyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={monthlyRankingFetcher} />
      </Suspense>
    </>
  );
};
```

### 再レンダリング時のサスペンド設計

ここからは、一旦最初の`PageB`に頭を戻して考えます。

```tsx
const PageB: FunctionComponent<{
  dailyRankingFetcher: Fetcher<User[]>;
  weeklyRankingFetcher: Fetcher<User[]>;
  monthlyRankingFetcher: Fetcher<User[]>;
}> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => {
  return (
    <>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={dailyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={weeklyRankingFetcher} />
      </Suspense>
      <Suspense fallback={<p>Loading users...</p>}>
        <Users usersFetcher={monthlyRankingFetcher} />
      </Suspense>
    </>
  );
};
```

これまでの議論では`PageB`が新規にマウントされた場合を考えていました。このときは中身の`Suspense`が新規にマウントされるので、その中の`Users`がサスペンドしても`useTransition`が反応しないのでした。

では、`PageB`がすでにマウントされている状態で、`startTransition`の中のステート更新に起因して`PageB`のpropsが変わった場合はどうでしょうか。新しくpropsから渡された`Fetcher`によってサスペンドした場合、それをキャッチするのは`PageB`がレンダリングした`Suspense`であることに変わりませんが、今回はこれらの`Suspense`はあらかじめマウントしてあった`Suspense`です。なぜなら、前回の`PageB`のレンダリングによってこの`Suspense`はすでにマウントされていたからです。よって、この場合は`useTransition`が働きます。

つまり、`PageB`は「新しくマウントされたときは`useTransition`に非対応だが、マウント済の状態でpropsが更新された時は`useTransition`に対応」という特徴を持つコンポーネントなのです。Concurrent Modeによほど精通していなければ、コンポーネントの定義を一目見てこのことを見抜くのは難しいでしょう。

この状態はなんだか一貫性がありませんね。これでも良いならばこの実装で問題ありませんが、どちらかに統一したいということもあるでしょう。まず、常に`useTransition`に対応にしたい場合の方法は先ほどまで述べた通りで、`Suspense`を消すなり、`Suspense`の中ではなく`PageB`自体がサスペンドするなりといった方法があります。

一方、常に`useTransition`非対応にしたい場合はどうすれば良いでしょうか。答えは、「propsが変わるたびに`Suspense`をマウントし直す」です。そうすることで`Suspense`は常に新しくマウントされた扱いとなり、その中でのサスペンドは`useTransition`に影響しなくなります。`Suspense`をマウントし直すには`key`を用います。Reactでは、同じコンポーネントでも異なる`key`が与えられた場合は別のコンポーネントと見なされますから、propsが変わるたびに`Suspense`に与える`key`を変えることで、`Suspense`をアンマウント→マウントさせることができます。

具体的な方法の一例としては、まず次のような`useObjectId`カスタムフックを用意します。

```ts
export const useObjectId = () => {
  const nextId = useRef(0);
  const mapRef = useRef<WeakMap<object, number>>();

  return (obj: object) => {
    const map = mapRef.current || (mapRef.current = new WeakMap());
    const objId = map.get(obj);
    if (objId === undefined) {
      map.set(obj, nextId.current);
      return nextId.current++;
    }
    return objId;
  };
};
```

このフックは`PageB`の中で次のように使います。今回のコードではそれぞれの`Suspense`に`key`が与えられており、`key`を計算するために`useObjectId`を使用しています。

```tsx
const PageB: FunctionComponent<{
  dailyRankingFetcher: Fetcher<User[]>;
  weeklyRankingFetcher: Fetcher<User[]>;
  monthlyRankingFetcher: Fetcher<User[]>;
}> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => {
  const getObjectId = useObjectId();
  return (
    <>
      <Suspense
        key={`${getObjectId(dailyRankingFetcher)}-1`}
        fallback={<p>Loading users...</p>}
      >
        <Users usersFetcher={dailyRankingFetcher} />
      </Suspense>
      <Suspense
        key={`${getObjectId(weeklyRankingFetcher)}-2`}
        fallback={<p>Loading users...</p>}
      >
        <Users usersFetcher={weeklyRankingFetcher} />
      </Suspense>
      <Suspense
        key={`${getObjectId(monthlyRankingFetcher)}-3`}
        fallback={<p>Loading users...</p>}
      >
        <Users usersFetcher={monthlyRankingFetcher} />
      </Suspense>
    </>
  );
};
```

`useObjectId`フックは関数`getObjectId`を返します。この関数は各オブジェクトに対して異なるIDを返します。同じオブジェクトに対しては何回呼んでも同じIDが返されます。これを`key`に組み込むことによって、`daylyRankingFetcher`などに別の`Fetcher`が渡されたタイミングで`Suspense`に渡される`key`も更新され、新たな`Suspense`がマウントされた扱いになります。

この実装により、`PageB`が別のpropsで再レンダリングされた場合でも、内部で発生するサスペンドの影響を内部に封じ込めて`useTransition`に影響させないことが可能になりました。

### `Suspense`と`useTransition`の関係を整理する

ここまでは、`Suspense`と`useTransition`の関係を解説し、ユースケースに合わせた実装法を紹介してきました。なんだか場当たり的な印象を受けた読者の方も多いと思いますので、もう少し整理して見直してみましょう。

あるコンポーネントがPromise（をラップする`Fetcher`）を受け取るとします。そのコンポーネントの責務がそのデータを表示することであれば、必然的にそのコンポーネントはサスペンドを発生させることになります。

コンポーネント内で発生しうるサスペンドは3種類に分類できます。3種類のサスペンドは、「コンポーネントの外にサスペンドが出て行くかどうか」と「`useTransition`に対応するかどうか」に注目すると次の表のようにそれぞれ異なる特徴を持ちます。

| | サスペンドの種類 | 外に出て行くか | `useTransition`対応 |
|:-:|:--|:--|:--|
| 1 | 内部の`Suspense`でキャッチされないサスペンド | Yes | Yes[^note_outer_suspense] |
| 2 | 内部で新規にマウントされた`Suspense`にキャッチされるサスペンド | No | No |
| 3 | 内部の既存の`Suspense`にキャッチされるサスペンド | No | Yes |

[^note_outer_suspense]: このコンポーネントの外部に設置された既存の`Suspense`にキャッチ場合は`useTransition`に反応しないサスペンドとなりますが、それはこのコンポーネントの預かり知るところではありません。このコンポーネントが`useTransition`に対応する可能j性を消しているわけではないことからYesとしています。

パターン1が一番スタンダートなサスペンドでしょう。あるコンポーネントが`Fetcher`から得たデータを表示することが責務ならば、データがまだない場合にそのコンポーネントがサスペンドするのは自然なことです。

パターン2は逆にサスペンドを完全に内部で抑え込むパターンです。サスペンドが発生しても、そのことはコンポーネントの外部には検知されません。データがまだ無いときの挙動を完全にコンポーネント内で制御したい場合に適しています。

パターン3は、コンポーネントが新規にマウントされた場合は発生せず、再レンダリングのときのみ可能な選択肢です。これは扱うのがやや難しいですが、コンポーネントの内部で`useTransition`を使いたい場合などはこれが一番自然な選択肢となることが多いでしょう。

コンポーネントのロジックを実装する際には、これらを組み合わせることもあるでしょう。例えば、先ほど出てきた`Fetcher.race`の例は1と2の合わせ技です。

コンポーネントの使い勝手という観点からは、パターン1が最も有利です。パターン1はコンポーネントの外側に`Suspense`を配置すれば2や3に変換できますが、逆に2や3を1に変換することはできないからです。

パターン1と2や3の使い分けはコンポーネントの責務に応じて決めるのが良いでしょう。具体的には、データがない場合にフォールバックを表示するという責務をコンポーネントが持っているのであれば、2か3を選択することになります。逆に、その責務を持たずデータがない場合はサスペンドすべきならば、1を選択しなければなりません。

### 誰が`Fetcher`を用意するのか

Concurrent Modeにおいては、誰かいつ非同期処理を開始する（`Fetcher`を用意する）のかがとても重要です。従来の基本的なパターンは、データを表示する責務を持ったコンポーネントが`useEffect`の中で非同期処理を開始するというものです。`Fetcher`と組み合わせればこのような実装になるでしょう。

```tsx
const PageB: FunctionComponent = () => {
  const [dailyRankingFetcher, setDailyRankingFetcher] = useState<
    Fetcher<User[]> | undefined
  >(undefined);
  useEffect(() => {
    setDailyRankingFetcher(new Fetcher(() => fetchUsers()));
  }, []);

  return dailyRankingFetcher !== undefined ? (
    <Users usersFetcher={dailyRankingFetcher} />
  ) : null;
};
```

しかし、2つの理由からこの実装は忌避すべきです。一つ目の理由は、一度レンダリングされたあと`useEffect`内ですぐに再度レンダリングを発生させていることです。これはReactにおける典型的なアンチパターンの一つです。

もう一つの理由は、こうすると`PageB`が自動的に`useTransition`に非対応になるからです。`PageB`が最初にレンダリングされたときはまだサスペンドが発生しませんから、`PageB`に遷移するきっかけとなったステート更新ではサスペンドが発生しなかったことになります。もし`PageB`に遷移するときに`useTransition`を使いたければ、このような実装は必然的に選択肢から除外されます。

では、どうすればよいのでしょうか。大きく分けて2つの選択肢があります。基本的には、これまでやってきたように外から`Fetcher`を渡すことになります。これについては次回の記事で詳しく扱います。

もう一つ、`useEffect`の中ではなく最初のレンダリング中に直に`Fetcher`を用意するという戦略を思いついた方もいるかもしれません。しかし、ほとんどの場合これは無理筋です。

#### `useState`で`Fetcher`を用意することはできない

例えば、次のような実装を試してみましょう。`useState`は関数を渡すと最初のレンダリング時にその関数が呼び出されてステートの初期化に用いられます。次のようにすることで`dailyRankingFetcher`をいきなり`Fetcher`で初期化し、初手でサスペンドを発生させることができます。

```tsx
const PageB: FunctionComponent = () => {
  const [dailyRankingFetcher] = useState(() => new Fetcher(() => fetchUsers()));

  return <Users usersFetcher={dailyRankingFetcher} />;
};
```

しかし、これは期待通りに動きません。`PageB`はずっとサスペンドしたままになります。その理由は、`PageB`がレンダリングされるたびに新しい`Fetcher`インスタンスが生成されるからです。

`PageB`が最初にレンダリングされた場合は`useState`に渡された関数が呼ばれて新しい`Fetcher`インスタンスが`dailyRankingFetcher`に入ります。ここまでは想定通りですが、その後サスペンド明けに`PageB`が再度レンダリングされたとき、`PageB`は初回レンダリングという扱いになります。よって、`dailyRankingFetcher`に入るのはまた新しく作られた`Fetcher`インスタンスとなり、`PageB`は再度サスペンドします。これを繰り返すことになり、`PageB`は永遠に内容をレンダリングすることができません。

すなわち、レンダリングの結果サスペンドが発生したときはレンダリングが完了したと見なされず、`useState`フックなどの内容はセーブされません。あたかも、そのレンダリングが無かったかのように扱われます。`useMemo`なども同じです。

この性質により、「最初にサスペンドしたレンダリング」から「サスペンド明けのレンダリング」に情報を渡すことは自力では困難です。そのため、最初のレンダリングの中で作った`Fetcher`インスタンスをサスペンド明けのレンダリングで手に入れることができず、サスペンドが空けても何をレンダリングすればいいか分からなくなってしまいます。`Fetcher`をpropsで外から受け取ることでこの問題は回避できるのです。

### サスペンドとコンポーネントの純粋性

`useState`がだめなら`useRef`なら、と思った方もいるかもしれませんが、実は`useRef`でも無理です。`useRef`はレンダリングをまたいで同じオブジェクトを返すのが特徴でしたが、`useRef`によって返されるオブジェクトは最初のレンダリングで作られます。よって、「最初のレンダリング」が何回も繰り返されれば毎回新しいrefオブジェクトが作られることになり、やはりサスペンド前後の情報の受け渡しは困難です。

ただし、最初のレンダリング以外の場合は注意が必要です。そもそも最初のレンダリング以外であっても、サスペンドしたレンダリングの結果は残りません。例えば、`useMemo`はサスペンドしたレンダリングにおいて計算された値はキャッシュしません。そのレンダリング中に値を計算したという事実が無かったことにされるからです。

しかし、`useRef`は「毎回同じオブジェクトを返す」のが役割ですから、初回以外であればサスペンドしたレンダリングとサスペンド明けのレンダリングでは`useRef`から同じオブジェクトが返されます。これを用いることで、サスペンドしたレンダリングから何らかの情報を残すことができます。

明らかに、**このようなことは避けるべき**です。それは、このような`useRef`の使用は**レンダリングの純粋性**を破壊しているからです。レンダリングの純粋性とは、「コンポーネントをレンダリングしても副作用が発生しない」という意味で、「意味もなくコンポーネントをレンダリングしても（＝関数コンポーネントを関数として呼び出しても）安全である」という意味でもあります。

Concurrent Modeにおいては「コンポーネントがレンダリングされた（関数コンポーネントとして呼び出された）」ことは「そのコンポーネントのレンダリング結果がDOMに反映される」ことを意味しません。サスペンドが発生する可能性があるからです。この状況下でReactが好き勝手にレンダリングを試みるための前提として、コンポーネントは純粋であるべきとされているのです。

実際、Reactでは副作用は`useEffect`内で行うように推奨しています。`useEffect`はコンポーネントが実際にDOMにマウントされた場合にコールバックが呼び出されます。サスペンドによりDOMに反映されなかった場合はコールバックは発生しません。

また、レンダリングが純粋であることを強調するためか、Conncurrent Modeではデフォルトで1回のレンダリングで関数コンポーネントが2回呼び出されるようになっています（おそらくproductionでは1回）。これは、純粋でないコンポーネントを作ってしまった際に発生するバグを検出しやすくするためでしょう。実は先ほどの`useState`のサンプルでも、1回`PageB`がレンダリングされるたびに`Fetcher`インスタンスが2個作られていました。非同期処理を発生させるのも副作用ですから、そもそも`useState`のステート初期化時に`Fetcher`インスタンスを作るのは無理筋だったということになります。

`useRef`に話を戻しますが、Concurrent Modeでは**refオブジェクトへのアクセス（特に書き込み）は副作用である**と考えるべきです。先ほど説明したように、レンダリング中にrefオブジェクトに書き込むと、サスペンドしたレンダリングの影響がそれ以降に残ってしまうため、コンポーネントのレンダリングが純粋でなくなるからです。refオブジェクトは、`useEffect`のコールバック内やイベントハンドラなど、副作用が許された世界でのみアクセスすべきです。refオブジェクトはもはや完全に副作用の世界の住人なのです。

目ざとい方は、先程出てきた`useObjectId`は`useRef`に書き込んでいたじゃないかと思われるかもしれません。それはそのとおりなのですが、実は`useObjectId`はレンダリングの純粋性を損なわないように注意深く実装されています。純粋性を壊さない注意深い実装ならば、`useRef`を使える可能性もあるのです。無理なときは無理なので無理だと思ったら潔く諦めるべきですが。

## まとめ

この記事では、サスペンドを念頭に置いたコンポーネント設計をどのようにすべきかについて議論しました。

重要なのは、サスペンドはその発生の仕方によって3種類に分類できるということです。さらに、これらを組み合わせることでより複雑なパターンを実装することもできます。もちろん、コンポーネントの記述は宣言的な書き方が保たれています。

Concurrent Modeでは、あるコンポーネントがどのような状況下でどの種類のサスペンドを発生させるのかということをコンポーネント仕様の一部として考えなければなりません。これは特に`useTransition`と組み合わせるときに重要です。Concurrent Mode時代のコンポーネント設計では、コンポーネントの責務は何なのかということを冷静に見極めて、そのコンポーネントはどのようにサスペンドすべきかということを考えなければならないのです。

記事の後半では、Concurrent Modeでは特にレンダリングの純粋性が重要であることを開設しました。これを踏まえると、初手でサスペンドするコンポーネントは必然的に`Fetcher`を外部から受け取ることになります。

次回の記事では、誰が`Fetcher`を作ってどう受け渡すのかについて考えていきます。

次の記事: [Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する](https://qiita.com/uhyo/items/52ab8e2cab30b0811294)