React の最上位 API サスペンス (Suspense)についてメモ

## Suspense

`<Suspense>` コンポーネントは React の最上位 API に含まれる。`<Suspense>`は、React 16.6以降で導入されたコンポーネントで、非同期な操作（たとえば、データのフェッチやコードの分割）をサポートするために使用される。

`<Suspense>`コンポーネントは、React アプリケーションで非同期な処理が完了するまで待機し、その間に指定されたフォールバック（代替）コンポーネントを表示することができる。
これは、特にコードの分割（Code Splitting）やデータの遅延読み込みなどの場面で有用である。

以下[公式ドキュメント](https://ja.react.dev/reference/react/Suspense)より引用。


```jsx
<Suspense fallback={<Loading />}>
  <SomeComponent /> //children
</Suspense>
```

**children:** レンダーしようとしている実際の UI です。children がレンダー中にサスペンド（suspend, 一時中断）すると、サスペンスバウンダリは fallback のレンダーに切り替わります。

**fallback:** 実際の UI がまだ読み込みを完了していない場合に、その代わりにレンダーする代替 UI です。有効な React ノードであれば何でも受け付けますが、現実的には、フォールバックとは軽量なプレースホルダビュー、つまりローディングスピナやスケルトンのようなものです。children がサスペンドすると、サスペンスは自動的に fallback に切り替わり、データが準備できたら children に戻ります。fallback 自体がレンダー中にサスペンドした場合、親のサスペンスバウンダリのうち最も近いものがアクティブになります。


### すでに表示されているコンテンツが隠れるのを防ぐ 

下記のコードの場合、コンポーネントがサスペンドすると、直近の親のサスペンスバウンダリがフォールバック表示に切り替わる。すでに何らかのコンテンツを表示していた場合、これによりユーザ体験が不快になる可能性がある。

```jsx
import { Suspense, useState } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');

  function navigate(url) {
    setPage(url);
  }

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}

```

サイト全体のレイアウトが BigSpinner に置き換えられてしまう。
これを防ぐため、ナビゲーションの state 更新を`startTransition`でトランジションとしてマークすることができる。

```jsx
import { Suspense, startTransition, useState } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');

  function navigate(url) {
    startTransition(() => {
      setPage(url);
    });
  }

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}
```

`startTransition`を使用することで、React に対して state の遷移が緊急のものではなく、既に表示されている内容を隠すよりも前のページを表示し続ける方が良いと伝えることができる。

### startTransitionについてより詳しく知る

`startTransition`は、React18から導入された新しいAPIであり、Reactの非同期な更新を制御するためのもの。

この API は、ユーザーエクスペリエンスを向上させるために非同期な操作をスケジューリングするために使用される。

`startTransition`を使うと、Reactは非同期な更新の開始を通知し、その操作がブラウザのアイドルタイムで行われるようにスケジューリングする。

これにより、リソースが利用可能なときに非同期な操作が実行され、ユーザーエクスペリエンスが向上することが期待される。例えば、ページ遷移や非同期データの読み込みなどがこれに該当する。

`startTransition`を使って非同期なトランジションを開始することができる。

```jsx
import { startTransition, useState } from 'react';

function MyComponent() {
  const [data, setData] = useState(null);

  const fetchData = async () => {
    // 非同期なデータの取得
    const result = await fetchDataFromServer();

    // startTransitionを使用して非同期なトランジションを開始
    startTransition(() => {
      setData(result);
    });
  };

  return (
    <div>
      <button onClick={fetchData}>Fetch Data</button>
      {data && <div>{data}</div>}
    </div>
  );
}
```

この例では、fetchData 関数が非同期なデータを取得し、startTransition 内で新しいデータを設定している。これにより、データの取得が非同期であるときに、トランジションが開始され、ユーザーエクスペリエンスが向上する。

重要なのは、`startTransition`は非同期な更新をスケジューリングするだけであり、即座にコンポーネントの再レンダリングをトリガーしない点。そのため、Reactはアイドルタイムに非同期な操作を実行し、完了後に適切なタイミングでコンポーネントを再レンダリングする。

一般的なReactの挙動では、状態が変更されると、すぐにコンポーネントが再レンダリングされる。しかし、非同期な操作が多くの計算資源やネットワークリソースを必要とする場合、その処理が完了するまでに時間がかかり、ユーザーエクスペリエンスが悪化する可能性がある。

`startTransition`を使用することで、Reactは非同期な操作の完了を待たずに、一旦コンポーネントの再レンダリングを保留し、アイドル状態になったときに再レンダリングを実行する。これにより、ユーザーがブラウジングしている間に滑らかで応答性の高いエクスペリエンスが提供される。

`startTransition`はReactに「この非同期な操作は優先的に処理してください。ですが、すぐに再レンダリングはしないでください。アイドル状態で実行してください」と指示するための手段である。

### トランジションが進行中であることを示す 

`startTransition`を`useTransition`に置き換えることで、ブーリアン型の`isPending`値が得られる。

```jsx
import { Suspense, useState, useTransition } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');
  const [isPending, startTransition] = useTransition();

  function navigate(url) {
    startTransition(() => {
      setPage(url);
    });
  }

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout isPending={isPending}>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}
```

startTransitionを使用して更新を低緊急度としてマークすることができる。
また、 useTransition に置き換えることでナビゲーションが進行中であることを視覚的に示すことができる。
トランジション中、Reactは十分なデータがロードされるまで待機し、**不要なフォールバックが表示されるのを防ぐこと**が重要。

## 参考

https://ja.react.dev/reference/react/Suspense