Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する

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

## シリーズ一覧
- [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) コンポーネント設計にサスペンドを組み込む](https://qiita.com/uhyo/items/4c45672874874a2aad11)
- **Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する**
- [Concurrent Mode時代のReact設計論 (6) Concurrent Modeと副作用](https://qiita.com/uhyo/items/fa443351fd4444dacaf7)
- Concurrent Mode時代のReact設計論 (7) ステート管理ライブラリの展望（仮）
- Concurrent Mode時代のReact設計論 (8) まとめ（仮）

## トランジションを軸に設計する

Concurrent Modeの目玉機能の一つである`useTransition`については、これまでの記事でも何度も触れてきました。Concurrent Mode時代のアプリ設計において最も厄介なのがこの`useTransition`であると言っても過言ではなく、まさに`useTransition`を制する者がConcurrent Modeを制するといえます。

今回は、`useTransition`とは結局何なのか、`useTransition`を設計に組み込むときの考え方について議論します。これまでは`useTransition`は「`startTransition`関数と`isPending`の値を得られるフック」と説明していました。機能面で見るとこれは間違いではありませんが、いまいち釈然としませんね。例えば`useState`なら「ステートを定義する」、`useEffect`なら「レンダリング後の副作用を定義する」といった、設計の側面から見たフックの効能を理解しなければなりません。

では、`useTransition`は何をするためのフックなのでしょうか。筆者の考えでは、それは**トランジションを定義すること**です。では、トランジションとは一体何なのでしょうか。

元来Reactにおいては、アプリのロジックの上では画面の変化は**ステートの更新**という形に抽象化されていました。ここまで見たように、Concurrent Modeでは新たに「変化後の画面をすぐ表示できない（＝サスペンドした）場合は前の画面にフィードバックを表示する」という要求を叶えられるようになります。このためにはReact本体によるサポートの一環として「サスペンド中の状態の検知」という機能が必要です。この部分を抽象化した概念が**トランジション**なのです。

ですから、`useTransition`の恩恵を受けるアプリを作るためには**トランジション**を組み込んだ設計が必要となります。

### トランジションの所有者

一般に、フックの呼び出しというのはコンポーネントに属するものです。例えば`useState`などは最もわかりやすい例で、これはそれが使われたコンポーネントに属するステートを宣言するものです。もちろん、`useTransition`も例外ではありません。つまり、**トランジションはコンポーネントに属する**のです。実際、`useTransition`を使うと`isPending`という真偽値が得られるのですから、その`isPending`を取り扱うのは`useTransition`を使ったコンポーネントの責任です。

ある意味で、`useTransition`は`useState`に近いフックです。それは、`isPending`というある種の状態を管理するフックだからです。そうなると、`isPending`という状態は誰が管理するのか、言い換えればどのコンポーネントが`useTransition`を呼び出すのかという問題が発生することになります。

末端のコンポーネントが`useTransition`を呼ぶのか、それとも上層のコンポーネントにまとめるのかというのは難しいテーマであり、筆者もまだ結論を出せてはいません。昨今のステート管理ライブラリはアプリのステートをコンポーネント間で分散させるのではなく一箇所のストアにまとめるという戦略を取っていることが多いですが、Concurrent Mode時代のステート管理ライブラリはそれに加えて`useTransition`も一緒に管理してくれるのかもしれません。ただ、複数のトランジションを定義するには複数回`useTransition`を呼び出すしかないので、上手に管理するのは難しそうですね。

### React公式の考え方

Reactの公式ドキュメントには、「ボタンのコンポーネントが`useTransition`を呼び出す」という例があります。この`Button`コンポーネントは`useTransition`を呼び出して`startTransition`関数を取得し、ボタンがクリックされたときに与えられた`onClick`を`startTransition`で包んで呼び出します。

```jsx
// ドキュメント (https://reactjs.org/docs/concurrent-mode-patterns.html) から引用
function Button({ children, onClick }) {
  const [startTransition, isPending] = useTransition({
    timeoutMs: 10000
  });

  function handleClick() {
    startTransition(() => {
      onClick();
    });
  }

  // ...

  return (
    <>
      <button onClick={handleClick} disabled={isPending}>
        {children}
      </button>
      {isPending ? spinner : null}
    </>
  );
}
```

`Button`に渡された`onClick`によって行われたステート更新によってサスペンドが発生した場合、`useTransition`の効果によりステート更新前の状態がレンダリングされます。ただし、その際`useTransition`が返す`isPending`が`true`になることから、ボタンはdisabledの状態になります。この状態はサスペンドが解消するまで（サスペンド時に投げられたPromiseが解決するまで）続きます。

話を戻すと、つまりこの`Button`コンポーネントはトランジションを内包しており、`onClick`で与えられたステート変化を自動的にトランジションに包んで行なってくれるうえに、サスペンド中の表示まで面倒を見てくれるというたいへん便利なコンポーネントだということです。コンポーネントを使う側は、`useTransition`のことなど意識せずにこの`Button`コンポーネントをボタンとして使うだけで`useTransition`の恩恵を受けることができます。

公式ドキュメントに載っているだけあって、これも`useTransition`のための設計におけるひとつの解です。しかしながら、これはあくまでお手軽な例であって、より複雑なアプリケーションに適用できる設計ではないというのが筆者の考えです。その理由は、この設計は以下に示す2つの問題点を抱えているからです。

#### 問題点1: トランジションとステート更新が分離している

関数`startTransition`は、コールバック中でステート更新を行なって初めて意味を持ちます。一方で、`Button`コンポーネントは`startTransition`のコールバック中で`onClick`を呼び出します。

つまり、`Button`コンポーネントは暗黙のうちに「`onClick`を呼び出すとステート更新が発生する」ことを要求しています。このことがインターフェースからは見えづらくなっています。言い方を変えれば、`startTransition`の呼び出しとその中でのステート更新という、本来一体となって書かれるべき処理が`Button`コンポーネントの内側と外側に分かれてしまっています。

ロジックを分割するのは良いこととはいえ、セットで意味を持つものを分断するのは逆にプログラムを分かりにくくするばかりで良くありません。

ただ、`startTransition`は中でステート更新を行わなくてもエラーなどが起きるわけではなく、ただ何も起きないだけです。なので、`onClick`内でステート更新を行わないのが有害なことかと聞かれればそうでもありません。

問題となるのは例えば「`startTransition`の中で何か非同期処理を行なったあとにステート更新を行う」というような場合で、非同期処理を挟んだ時点で`startTransition`の中という扱いではなくなるため、この場合は期待した動作とはならないでしょう。Concurrent Mode時代にそんなやり方で非同期処理を行うべきではないと言われれば返す言葉がありませんが、それはアプリ全体の設計に関わる話であり、こんな末端のutilityコンポーネントがアプリ全体の設計を意識しないと使えないというのはどうにも微妙です。

#### 問題点2: `startTransition`の中と外の併用ができない

ステート更新の全てを`startTransition`の中で行うのではなく、中と外の両方でステート更新を行いたいという場合もあります。
外で行われた更新はサスペンドしても`useTransition`のサポート対象となりませんが、その代わりにステート更新が即座に反映され、サスペンド中のレンダリング（`isPending`が`true`になった状態）にも反映された状態になります。

ですから、ユーザーへのフィードバックを素早く返しつつ非同期処理を行うというユースケースでは、フィードバックを返すために必要なステート更新は`startTransition`の外で行い、非同期処理に相当するステート更新は`startTransition`の中で行う必要があります。

先ほどの`Button`コンポーネントでは`onClick`は問答無用で`startTransition`の中に入れられるため、このようなユースケースに対応できません。

#### 中と外の併用例

「`startTransition`の中と外の両方でステートを更新するなんて、そんな面倒なケース本当にあるの？」と思われた読者の方もいるかもしれませんので、ひとつ例をお出しします。

最初の記事で宣伝したアプリでは、穴が空いたTypeScriptプログラムが提示され、ユーザーが全ての穴を埋めると正誤が判定されます。下のスクリーンショットは正しく穴を埋めた状態の表示で、プログラムの背景が緑色になると共に「NEXT」ボタンが表示されます。

<img width="572" alt="Type Puzzlesで空欄を全て埋めた状態のスクリーンショット" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233667/b27a6f82-3f59-b571-4ae2-54325d83b036.png">

正誤の判定はWorker上で動くTypeScriptコンパイラが行い時間がかかるため、非同期処理となります。ユーザーが最後の穴を埋めた瞬間に正誤判定処理を表す`Fetcher`が作成されます。この`Fetcher`は当然ステートに入れられますが、非同期処理であるためこのときの更新は`startTransition`の中で行う必要があります。実際、アプリでは正誤判定中に「evaluating...」という表示を行うために`useTransition`を使用しています。

一方、ユーザーが穴を埋めるという操作を行なった（プログラム中の穴を選択した状態で下の選択肢をクリックした）場合、穴が埋まるというUI上のフィードバックはなるべくすぐに行う必要があります。つまり、ユーザーが正誤判定を待っている間、画面は穴が全部埋まった状態で「evaluating...」と表示されることになります。

そのためには、穴を埋めるためのステート更新は`startTransition`の外で行う必要があります。もし中に入れてしまうと、正誤判定が終わるまで画面に反映されず、ユーザーは選択肢をクリックしたあと画面が無反応のまま待たされることになります。

以上のことから、ユーザーが選択肢をクリックした瞬間のステート更新は`startTransition`の中と外の両方で行う必要がありました。

なお、フィードバックといっても、`startTransition`の外で行なったステート更新は当然ながらサスペンド終了後も残り続けます。今回は、ユーザーが埋めた穴は正誤判定完了後も残り続けるのが正しいのでこれで問題ありません。あくまでサスペンド中にのみ一時的に表示したいという場合は`isPending`に頼ることになります。

#### `Button`コンポーネントの改善案

では、話を`Button`コンポーネントに戻しましょう。このコンポーネントが持つ2つの問題のうち、特に問題点2（`startTransition`の中と外を併用したステート更新ができない）が問題であり、`Button`コンポーネントの使途を大きく制限しています。

これをどう解決すべきかという話は、結局`useTransition`をどう設計に組み込むべきかという話そのままです。ですから、唯一解があるような話ではないのですが、とりあえず一つ案を出してみます。

それは、`onClick`にコールバックで`startTransition`を渡すというものです。この方針では、トランジションの所持者を`Button`にしたままで前述の2つの問題を解決します。具体的にはこのようなコードになるでしょう。

```jsx
// ドキュメント (https://reactjs.org/docs/concurrent-mode-patterns.html) から引用
function Button({ children, onClick }) {
  const [startTransition, isPending] = useTransition({
    timeoutMs: 10000
  });

  function handleClick() {
    onClick(startTransition);
  }

  // ...

  return (
    <>
      <button onClick={handleClick} disabled={isPending}>
        {children}
      </button>
      {isPending ? spinner : null}
    </>
  );
}
```

ポイントは`handleClick`で、中身は`onClick(startTransition);`となっています。このことから分かるように、`Button`を使う側はこのような使い方をすることになります。

```jsx
<Button onClick={(startTransition)=> {
  startTransition(()=> {
    setState(new Fetcher(()=> ...));
  });
}}>load something</Button>
```

こうすることで、前述の「`startTransition`とステート更新が`Button`の内外に分散している」及び「`startTransition`の中と外を併用したステート更新ができない」という2つの問題点が解消されていますね。`Button`コンポーネントの役割も「トランジションを内包しており、ボタンクリック時にそのトランジションを用いてステート遷移をすることができる」とより明確なものになりました。

トランジションを持つのが`Button`でいい（絶対に`Button`の中でしかフィードバックを返さないという強い意志がある）ならこの設計もありでしょう。

一方で、より広範囲にわたってフィードバックを表示したい場合は、その全体をカバーするためにより上流に`useTransition`を配置しなければいけません。

### どこにどれだけ`useTransition`を置くか

繰り返しますが、`useTransition`により宣言されるトランジションはコンポーネントに属するものです。そして、`useTransition`が`isPending`を返すことから、トランジションを所持するコンポーネントは必然的にそのトランジション内で発生したサスペンドに際してユーザーに適切なフィードバックを返す責任を負います。

ですから、その責任をどのコンポーネントが負うのかを考えて`useTransition`を配置する必要があります。これがトランジションに係るアプリ設計の基本です。もうひとつ、`startTransition`関数を入手できるというのも`useTransition`の機能ですが、こちらはコンポーネントツリーの上へも下へも受け渡しがしやすいものです。上に渡すのは先ほど見た`Button`の改善案のような感じです。下に渡すほうが簡単なのはいうまでもありませんね。一方で、`isPending`を上に受け渡すのは不可能です。ですから、フィードバックをレンダリングするコンポーネントかそれより上に`useTransition`を配置しなければいけません。

トランジションとステート更新はセットで扱われることや、`isPending`もステートに似た性質を持つものであることから、`useTransition`の配置の方針はステート（`useState`や`useReducer`）の配置の方針と似たものになります。ステートを上の方の1箇所で管理する（`useReducer`を使う場合などはこれになりがちですね）場合はそこに`useTransition`も配置するのが素直な設計です。一方で、ステートを分散させる方針の場合は`useTransition`も適宜分散させることが考えられます。

また、Reduxなどを使用する場合にありがちな、ステートを一元管理するような設計の場合でも、ちょっとしたフラグなどは末端のコンポーネントが持ってよいという説もあります。トランジションの最中かどうかを表す`isPending`もこれに近いところがありますから、必ずしもコンポーネントツリーの上の方に持っていく必要は無いかもしれません。トランジションもまた、ステート管理にまつわる終わりのない議論に組み込まれることになるということです。

また、複数の異なるトランジションを取り扱うためには複数の`useTransition`呼び出しが必要です。というのも、`useTransition`から返される`startTransition`と`isPending`は実はペアになっています。すなわち、`startTransition`中でサスペンドが発生した場合、同じ`useTransition`から返された`isPending`が`true`になります。

場合によって異なるフィードバックを使い分けたい場合は、トランジションを複数用意して使い分けることになります。このことから分かるのは、トランジションの本質が`isPending`を見てフィードバックを表示するところにあるということです。トランジションの設計とは、どのような場合にどのようなフィードバックが発生するかを整理して、それぞれに対してトランジションを定義することなのです。

別の言い方をすれば、我々は**同じ種類のステート更新をするときは同じトランジションを用い、違う種類のステート更新をするときは違うトランジションを用いるべきである**ということになります。

例えば、Reactアプリの典型的なトランジションの一つはページ遷移です。多くの場合、ページ遷移の待機中はどこからどこへのページ遷移かに依らずに同じようなフィードバックが返されます。例えばGitHubで画面上部にローディングバー的なものが出るようなイメージです。それを実現するためには、どのページ遷移にも同じトランジションを用いるべきです。ここから言えるのは、ページ遷移のトランジションは必然的に、各ページの中でなくより上位の（アプリ全体を統括する）コンポーネントで定義されるということです。

一方で、先ほどのアプリの例にあったような正誤判定を行うというトランジションの場合、それが発生するのは問題表示ページの中なので、ページ内にトランジションを定義することができます（もちろん、場合によってはより上に持っていくこともできるでしょう）。

### トランジションとステート更新の直交性

トランジションという概念が優れている点は、ステート更新との間に直交性があることです。つまり、サスペンドを伴うステート更新を行う際に「どのようなステート更新を行うか」と「どのトランジションを用いるか」の間に依存関係が無く、両者をそれぞれ自由に選ぶことができます。どのステート更新をする際にどのトランジションを使っても良いのです。逆に言えば、Concurrent Modeにおいては画面を更新する際には、ステートの更新ロジックに加えてトランジション（より具体的には`startTransition`関数）を調達しなければならないということです。

この直交性より、「非同期処理中は前の画面に留まる」という処理を「ステート更新（及びSuspenseによるサスペンドのサポート）」と「トランジション」の2つの要素に疎結合な形で分解することに成功しています。

例えば、複数の種類のステート更新に際して、同じトランジションを利用することができます。その典型例はやはりページ遷移の場合であり、どのページからどのページに遷移する場合でも同じトランジションを使うのが普通でしょう。同じトランジションというのは結果としては同じフィードバックエフェクトということになりますが、これを単なる「コードの共通化」ではなく「（実体として）同じトランジション」という一段上の抽象度で表現できることは特筆に値します。APIデザインの妙と言えるでしょう。


### 設計の具体例

ここまでをまとめると、`useTransition`はトランジションを定義するフックであり、アプリの設計に当たっては`useTransition`をコンポーネントツリーの中のどこで呼び出すかを`useState`と同様の観点から考えるべきであるということでした。

前回までに出てきた`Root`とか`PageA`とかの例を見直してみましょう。2つ前の記事では`Root`はこのように定義されていました。

```tsx
export const Root: FunctionComponent = () => {
  const [state, setState] = useState<AppState>({
    page: "A"
  });
  const goToPageB = () => {
    setState({
      page: "B",
      usersFetcher: new Fetcher(() => fetchUsers())
    });
  };
  return (
    <Suspense fallback={null}>
      <Page state={state} goToPageB={goToPageB} />
    </Suspense>
  );
};

const Page: FunctionComponent<{
  state: AppState;
  goToPageB: () => void;
}> = ({ state, goToPageB }) => {
  if (state.page === "A") {
    return <PageA goToPageB={goToPageB} />;
  } else {
    return <PageB usersFetcher={state.usersFetcher} />;
  }
};
```

ポイントは、画面Bに遷移するためのステート更新を行う関数`goToPageB`を`Root`が定義していることです。今いるページを表すステートを`Root`が持っているのでこれは自然ですね。

次に`PageA`はこのようになっていました。

```tsx
const PageA: FunctionComponent<{
  goToPageB: () => void;
}> = ({ goToPageB }) => {
  const [startTransition, isPending] = useTransition({
    timeoutMs: 10000
  });
  return (
    <p>
      <button
        disabled={isPending}
        onClick={() => {
          startTransition(() => {
            goToPageB();
          });
        }}
      >
        {isPending ? "Loading..." : "Go to PageB"}
      </button>
    </p>
  );
};
```

`PageA`は`useTransition`を呼び出すことでトランジションを定義しています。これは何のトランジションかというと、言わずもがな、画面Aから画面Bへの遷移のためのトランジションです。画面遷移時は、`PageA`内で定義したトランジション内で`goToPageB`を呼び出すことで画面Bへの遷移を行います。サスペンド中は画面A内のボタンが「Loading...」という表示になります。

この設計は、この記事の内容に照らせば微妙な設計です。なぜなら、`startTransition`と`setState`という、本来セットで扱われなければならない2つの関数がコンポーネント境界によって分断されているからです。

これに対しては2つの方策が考えられます。一つは`useTransition`を`Root`に移すもの、もう一つは`useTransition`を`PageA`の中に置いたままにするものです。先ほど述べたように、ページ遷移のためのトランジションであることを考えると`Root`に移すのが手であるように思えます。その一方で、今回トランジションに反応するのが`PageA`内のボタンであるという事情から、`useTransition`を`PageA`内に残す方向性にも一理あります。

まずは前者から見ていきましょう。

#### `Root`に`useTransition`を移す場合

この場合は`Root`はこんな感じになります。

```tsx
export const Root: FunctionComponent = () => {
  const [startTransition, isPending] = useTransition({
    timeoutMs: 10000
  });
  const [state, setState] = useState<AppState>({
    page: "A"
  });
  const goToPageB = () => {
    startTransition(() => {
      setState({
        page: "B",
        usersFetcher: new Fetcher(() => fetchUsers())
      });
    });
  };
  return (
    <Suspense fallback={null}>
      <Page state={state} isPending={isPending} goToPageB={goToPageB} />
    </Suspense>
  );
};

const Page: FunctionComponent<{
  isPending: boolean;
  state: AppState;
  goToPageB: () => void;
}> = ({ state, isPending, goToPageB }) => {
  if (state.page === "A") {
    return <PageA isPending={isPending} goToPageB={goToPageB} />;
  } else {
    return <PageB usersFetcher={state.usersFetcher} />;
  }
};
```

`Root`が`useTransition`を呼び出して、得られた`startTransition`は`goToPageB`に組み込まれました。これにより、`goToPageB`の中では`startTransition`と`setStage`がセットで扱われるようになり、設計上の問題点が解消されました。また、サスペンド中のフィードバック描画を`PageA`が担う関係で、`isPending`は`PageA`に渡されています。

この設計では`PageA`の中身が薄くなりますが、一応示しておくとこういう感じです。

```tsx
const PageA: FunctionComponent<{
  isPending: boolean;
  goToPageB: () => void;
}> = ({ isPending, goToPageB }) => {
  return (
    <p>
      <button disabled={isPending} onClick={goToPageB}>
        {isPending ? "Loading..." : "Go to PageB"}
      </button>
    </p>
  );
};
```

このように、`Root`に`useTransition`を移した場合はロジックが`Root`に偏重する形になります。昨今の風潮からするとこれは悪いことではありません。`PageA`に`state`と`isPending`を別々に渡しているのが微妙に思えた方もいるかもしれませんが、まあ適当なオブジェクトに詰め込むことも可能でしょう。

また、こうしてみると将来のステート管理ライブラリが`useTransition`も組み込んだ設計になりそうな予感がしてきます。そうすれば、`isPending`を自然にステートに混ぜ込んで扱うことができるかもしれません。

#### `PageA`に`useTransition`を残す場合

もう一つ考えられるのは`useTransition`を`PageA`に残す場合です。この場合は、先ほどReact公式ドキュメントの`Button`コンポーネントの例で議論したのと同じテクニックが使えます。つまり、`goToPageB`に`startTransition`を渡すのです。具体的に実装すると、以下のようになります。まず`Root`はこうです。`Root`が定義する`goToPageB`は`startTransition`を引数として受け取り、それを使用します。つまり、`goToPageB`は「与えられたトランジションの上で画面Bへのページ遷移を行う」という関数になります。

```tsx
export const Root: FunctionComponent = () => {
  const [state, setState] = useState<AppState>({
    page: "A"
  });
  const goToPageB = (startTransition: React.TransitionStartFunction) => {
    startTransition(() => {
      setState({
        page: "B",
        usersFetcher: new Fetcher(() => fetchUsers())
      });
    });
  };
  return (
    <Suspense fallback={null}>
      <Page state={state} goToPageB={goToPageB} />
    </Suspense>
  );
};

const Page: FunctionComponent<{
  state: AppState;
  goToPageB: (startTransition: React.TransitionStartFunction) => void;
}> = ({ state, goToPageB }) => {
  if (state.page === "A") {
    return <PageA goToPageB={goToPageB} />;
  } else {
    return <PageB usersFetcher={state.usersFetcher} />;
  }
};
```

次に、`PageA`はこのようになります。

```tsx
const PageA: FunctionComponent<{
  goToPageB: (startTransition: React.TransitionStartFunction) => void;
}> = ({ goToPageB }) => {
  const [startTransition, isPending] = useTransition({
    timeoutMs: 10000
  });
  return (
    <p>
      <button disabled={isPending} onClick={() => goToPageB(startTransition)}>
        {isPending ? "Loading..." : "Go to PageB"}
      </button>
    </p>
  );
};
```

`PageA`内で`useTransition`を用いて`startTransition`を取得し、それを`goToPageB`に渡しています。つまり、`PageA`がトランジションを定義し、`goToPageB`に使ってもらっています。

こちらの設計は`isPending`を`PageA`が持ってほしい場合に有利です。先ほども述べたように、このような設計にも一定の合理性があります。

筆者は、個人的にはこちらの設計のほうが好みです。その理由は、こちらの方がトランジションとステート更新の直交性を活かす余地があるからです。つまり、`goToPageB`は、`PageA`が定義したトランジションだけでなく、他のトランジションと組み合わせて使う余地があります。例えば、画面CからBに遷移したいときは画面Cの中で定義されたトランジションを使うということも、同じ`goToPageB`を用いて可能になります。

さらに言えば、次のような実装により`goToPageB`に「デフォルトのトランジション」を用意することすらできるでしょう。

```tsx
export const Root: FunctionComponent = () => {
  const [state, setState] = useState<AppState>({
    page: "A"
  });
  const [startDefaultTransition, isPending] = useTransition();
  const goToPageB = (
    startTransition: React.TransitionStartFunction = startDefaultTransition
  ) => {
    startTransition(() => {
      setState({
        page: "B",
        usersFetcher: new Fetcher(() => fetchUsers())
      });
    });
  };
  return (
    <Suspense fallback={null}>
      <Page state={state} goToPageB={goToPageB} />
    </Suspense>
  );
};
```

こうすることで、`goToPageB`はデフォルトでは`Root`があらかじめ用意しておいたトランジションを用いるが、`goToPageB`を使う側が用意した別のトランジションを使うこともできるというなかなか高性能な関数になります。

## まとめ

今回は`useTransition`をどのように使うかについて議論しました。この記事では、そもそも`useTransition`は**トランジション**を定義するものであると位置付けています。Reactの公式ドキュメントでも、`useTransition`はステートの更新をトランジションで包むためのものであるという説明があります。

トランジションはConcurrent Modeの目玉機能であり、サスペンドという部分は比較的分かりやすくて設計に議論の余地がないことも踏まえると、トランジションの設計がアプリの動作設計の軸に添えられがちです。

どのコンポーネントがトランジションを定義すべきかという点については、基本的にはステートと同じ考え方が通用します。ローディング中のフラグ用にステートを定義するのと同じ気持ちでトランジションを定義するのがよいでしょう。

気をつけるべきことは、トランジションとステート更新をむやみに分離しないことです。ただし、その一方でトランジションとステート更新の直交性を活かすことも重要です。筆者お勧めのパターンは、記事中で何度か示したように`startTransition`を受け渡すパターンです。

次回の記事はアプリ設計に残る最後の謎である「副作用」についての話です。

次の記事：[Concurrent Mode時代のReact設計論 (6) Concurrent Modeと副作用](https://qiita.com/uhyo/items/fa443351fd4444dacaf7)