Chapter 03

まずはSuspenseを試してみよう

uhyo
uhyo
2021.12.28に更新

では、ここからは手を動かしていきましょう。本書のコードは以下のリポジトリで参照することができます。Viteを使用しているので、npm installしたらnpm run devで開発サーバーを起動してコードを動かしましょう。

https://github.com/uhyo/react-suspense-handson

初期状態はmasterブランチにあります。ここからスタートしましょう。この章の内容を反映したブランチはchapter/try-suspenseです。

初期状態の確認

初期状態では、何の変哲もないReactアプリです。App.tsx内の以下のコードにより画面に「React App!」と表示されているはずです。

<div className="text-center">
  <h1 className="text-2xl">React App!</h1>
</div>

とにかくコンポーネントをサスペンドさせてみる

何はともあれ、まずはコンポーネントをサスペンドさせてみましょう。

コンポーネントをサスペンドさせるには、Promiseをthrowすればいいのでしたね。ということで、常に「1秒後に解決されるPromise」を投げるコンポーネントを作ってみましょう。

function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

const AlwaysSuspend: React.VFC = () => {
  throw sleep(1000);
};

これにより、AlwaysSuspendをレンダリングしようとすると常にサスペンドするはずです。

では、このコンポーネントをAppに組み込んでレンダリングし、サスペンドの挙動を確認してみましょう。

 <div className="text-center">
   <h1 className="text-2xl">React App!</h1>
+  <AlwaysSuspend />
 </div>

結果は、画面が真っ白になるです。コンソールには次のエラーが表示されているはずです。

Uncaught Error: AlwaysSuspend suspended while rendering, but no fallback UI was specified.

Add a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.

よくよく考えればこのようなエラーが発生するのは当たり前です。サスペンドというのはコンポーネントがレンダリングできない状態です。そもそも関数コンポーネントはその返り値がレンダリングされるのに、AlwaysSuspendは何も返していないですね。ということで、この状態ではAlwaysSuspendのレンダリング結果が存在しない状態なので、Reactはレンダリングを完遂できないのです。

ちなみに、サスペンドが発生したらその部分だけでなく周りも巻き込んで表示できなくなる点に留意しましょう。「AlwaysSuspend部分だけ何も表示されない」のような挙動ではなく、「App全体が表示できない」という挙動になるのです。これはReactが提供する一貫性保証の一部であり、ある瞬間にレンダリングされたコンポーネントツリーが部分的に表示されてしまうようなことを防ぐためであると思われます。全部表示できるか、全部表示できないかのどちらかなのです。

ということで、次はこのエラーに対処しましょう。

Suspenseで囲む

エラーの対処方法は簡単です。エラーメッセージにも書いてある通り、AlwaysSuspendSuspenseで囲めばよいのです。

 <div className="text-center">
   <h1 className="text-2xl">React App!</h1>
+  <Suspense fallback={<p>Loading...</p>}>
    <AlwaysSuspend />
+  </Suspense>  
 </div>

こうすると画面が復活します。「React App!」の下に「Loading...」が表示されます。Suspenseコンポーネントを追加したことで、その内部のサスペンドにこのコンポーネントが対処してくれているのです。Suspenseの内部でサスペンドが発生した場合、Suspense内部は相変わらずレンダリングできませんが、そこにフォールバックコンテンツを表示することによってサスペンドの影響を押さえ込んでくれます。

ここから、Suspenseコンポーネントが実はサスペンドの境界を定義する役割を持っていることがお分かりになるでしょう。先ほど「サスペンドは周りも巻き込んで表示できなくなる」と説明しましたが、その影響はSuspenseの外に波及しません。Suspenseの中がサスペンドしてレンダリングできなかったとしても、外側は通常通りにレンダリングできます。

逆に言えば、Suspenseの中でサスペンドが発生した場合、そのSuspenseの中は全部巻き込まれてレンダリングできなくなります。例えば次のようにしても、「ここは表示される?」は表示されません。

 <div className="text-center">
   <h1 className="text-2xl">React App!</h1>
   <Suspense fallback={<p>Loading...</p>}>
+   <p>ここは表示される?</p>
    <AlwaysSuspend />
   </Suspense>  
 </div>