RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

【React】Suspense・SWRは何を解決するのか

こんにちは!フロントエンド開発課所属のkoki_matsuraです!
今回はものすごく今更感が否めないのですが、Reactのv18で発表された「Suspense」とVercel社が提供しているReact Hooksライブラリの「SWR」によって何を解決してくれるのか、コンポーネントの表示と実装を例に紹介します。
目次は以下のようになっています。

Suspenseとは

React16.6で実験的な機能として追加され、React18で正式に追加された機能で、「コンポーネントのローディング状態をハンドリングする」ことが役割となっています。
基本的にこれだけなのですが、これによりコンポーネント単位でのロードを可能にします。

SWRとは

データ取得のためのライブラリです。
名前の由来はHTTPキャッシュ無効化戦略の"stable-while-revalidate"です。
こちらもやっていることはシンプルで特定のデータをキャッシュし、もう一度、必要になった際にキャッシュからデータを返します。また、定期的に裏側でフェッチをし、最新のデータに更新してくれます。

Suspense・SWRが解決すること

結論からいきます。 Suspenseは以下のことを解決します。

SWRは以下のことを解決します。

  • キャッシュにより、更新時の長期のローディングがなくなる
  • API通信が簡略化される
  • フェッチしたデータの管理をしなくてよくなる

よくSuspenseは「ローディング状態をハンドリングするもの」のように思ってしまっている方がいます。もちろん間違ってはいませんが、それだけではないということです。
むしろ、Suspenseのいいところは「コンポーネントの責務を明確にする」部分です。これが本質だと思っています。
SWRはキャッシュの管理や非同期処理状態の管理をしてくれるため、データのロードを待つ必要がなかったり、データの取得を非常にシンプルにしてくれます。

ここからはコンポーネントの表示と実装を例に、実際にSuspenseとSWRが解決していることを紹介していきます。

Suspense・SWR導入におけるコンポーネント表示の変化

表示においてSuspenseとSWRを取り入れることでどのような変化があるのかを紹介します。また、更新時においての違いも見ていきます。
下記のパターンで変化を見ていきます。

  1. Suspense なし SWR なし
  2. Suspense あり SWR なし
  3. Suspense あり SWR あり

下図のようにページに3つのコンポーネントを表示する例で比較します。それぞれのコンポーネントは上から1秒間、2秒間、3秒間かかるフェッチ処理を行っているため、表示するにも1秒間、2秒間、3秒間以上かかります。

Suspense なし SWR なし

まずはSuspenseもSWRも使っていない例です。初期表示時と更新時の動画を載せています。

・初期表示時

・更新時

初期表示時は「Suspenseを利用しない」というボタンを押したときに該当のページをリクエストします。そこから3秒間経過したのち、3つのコンポーネントが揃った状態で画面が表示されています。
更新時も3秒後に「◯秒後に表示されるコンポーネント_」の後ろについている文字列が一気に変更されていることがわかります。
これはサーバが完全にHTMLを構築してからクライアントに返していることを表しています。
図で表すと以下のようになります。

  1. ページをリクエス
  2. APIサーバへリクエスト(3つ)
  3. 1,2,3秒後にレスポンスをWEBサーバへ返却
  4. WEBサーバは全て揃ってからHTMLを構築
  5. クライアントへHTMLを返却
  6. HTMLを描画し、JSをロード、HTMLにハイドレーションを実行

少し雑に紹介していますが、大まかな流れを表せていると思います。初期表示や更新時に毎回3秒待たされるのは問題です。また、JSのロードやハイドレーションのことを考えると操作できるようになるには3秒よりもかかると考えられます。
ここにSuspenseを導入し、ページのロード単位をコンポーネント単位にすることで大きな待ち時間を解決してくれます。

Suspense あり SWR なし

Suspenseを導入すると以下のように変化します。

・初期表示時

・更新時

初期表示時は「SWRなし」というボタンを押すとページがリクエストされ、すぐにページが遷移しています。そして、1,2,3秒後にそれぞれのコンポーネントが順に表示されていくことが確認できます。取得中は代わりのコンポーネント(今回はローディングコンポーネント)が表示されます。
更新時も同様に、全体が取得中になり、1,2,3秒でコンポーネントが表示されていきます。
WEBサーバはHTMLが未完成の状態でも返してくれることがわかると思います。
図で表すと以下のようになります。少し見にくくなっています。すみません。

  1. ページをリクエス
  2. APIサーバへリクエスト(3つ)
  3. レスポンスがいらない部分だけのHTMLを構築
  4. クライアントへ3で作成したHTM Lを返却
  5. HTMLを描画、JSをロード、ハイドレーション。取得中の部分は代わりのコンポーネントを表示
  6. 「1秒後のコンポーネント」のレスポンスをWEBサーバへ
  7. 「1秒後のコンポーネント」のHTMLを構築
  8. 「1秒後のコンポーネント」を返却
  9. 「1秒後のコンポーネント」を表示し、その部分のJSをロード、ハイドレーション
    以下略...

10〜16は6〜9と同じ流れになるので省略しました。
Suspenseを使うことでコンポーネント単位でのロード(JSのロード・ハイドレーション)を可能にすることにより、初期表示の大きな待ち時間を解決してくれています。
しかしながら、更新するたびに完全に表示し直すのに結局3秒以上かかるのは問題です。
ここをSWRのキャッシュ管理により解決します。

Suspense あり SWR あり

SWRを導入すると、以下のようになります。
※ SWRを導入すると、SSRはできません。CSRになります。

・初期表示時

・更新時
初期表示はSuspenseのみを導入した時と見た目は変わりません。
見た目は変わらないのですが裏側ではフェッチと共にデータをキャッシュしており、更新時にはそのキャッシュを返すことですぐに表示することができています。つまり、SWRにより更新時の長期のローディングが解決されていることがわかります。
また、裏で再フェッチしてくれているので最新情報を取得できればすぐにコンポーネントが更新されていることが文字列が変更されていることで確認できます。

コンポーネントの表示で比較することでSuspenseのコンポーネント単位のロードによる表示速度の解決とSWRのキャッシュ管理による更新時の表示速度の解決を実際に見て、理解できたと思います。
次は、コード側から見てSuspenseとSWRが解決してくれることを紹介します。

Suspense・SWR導入におけるコンポーネント実装の違い

こちらも下記の3パターンで変化を見ていきます。先ほどとは違って、SWRだけを導入した場合とそこにSuspenseを導入した場合で紹介させていただきます。

  1. Suspense なし SWR なし
  2. Suspense なし SWR あり
  3. Suspense あり SWR あり

ユーザ情報を表示するコンポーネントを例にします。

Suspense なし SWR なし

下記はSuspense・SWRを共に使っていないUserコンポーネントで、ユーザ情報を表示するためだけのシンプルなコンポーネントです。

const User = () => {
  const [user, setUser] = useState<User>(null);

  useEffect(() => {
    fetchUser().then((res) => setUser(res));
  }, []);

  if (user === null) return <Loading />;

  return <Contents user={user} />;
};

データがnullの間、つまり取得中の間はローディングを表示、nullじゃなければ、ユーザ情報を表示するようになっています。そのユーザ情報はuseEffect内のfetchUser()で取得され、stateにより管理されています。
まず、問題として挙げられるのは、useEffect内でフェッチをしている点です。本来、useEffectは副作用処理を書くためのものであって、冪等性のない処理を書かない方が望ましいです。 また、Userコンポーネントの中でLoadingとContentコンポーネントを出し分けが行われている点も問題です。今回は例に挙げなかったのですが、フェッチのエラー処理もすることがあります。そうなると、Errorコンポーネントも出し分けに加わることになります。
本来はユーザ情報を表示することのみが仕事のUserコンポーネントがローディング、エラー、ユーザ情報を表示するコンポーネントになってしまいます。 これらの問題をSWRを導入した時、どのように変わるか見ていきましょう。

Suspense なし SWR あり

先ほどのコンポーネント表示ではSWRによってキャッシュが働くことを確認できたと思います。
コードは以下のようになります。

const User = ()=> {
   const {data: user, isLoading} = useSWR("user", fetchUser);
  
  if (isLoading) return <Loading />;
  
  return user && <Contents user={user} />;

state管理・useEffectがなくなり、その代わりにuseSWRフックを使っています。
useSWRはAPI通信を簡素化してくれるため、本来書くべきではないuseEffect内でのフェッチ処理やフェッチ後のデータ管理、キャッシュの管理の問題を解決することができます。
今回は書いていませんが、useSWRはエラーにも対応しています。
SWRだけでも非常にシンプルになりましたが、まだUserコンポーネントはローディングとユーザの出し分けという複数の責務を持ってしまっています。ここにSuspenseを取り入れてみます。

Suspense あり SWR あり

Suspenseを取り入れたUserコンポーネントは以下のようになります。

const User = ()=> {
  const { data: user } = useSWR("user", fetchUser, {
    suspense: true,
  });
  
  return <Contents user={user} />;

SWRでSuspenseを使いたい時はuseSWRの第三引数でsuspenseオプションをつけるだけです。
変更点はローディングの出し分けがなくなった部分です。
SuspenseがLoadingコンポーネントを出す責務を担ってくれました。たった一行減っただけですが、これでコンポーネントの責務の不明確化が解決できました。
ちなみに、Suspenseを使うときは該当のコンポーネントをSuspenseコンポーネントで囲むだけで実装できます。
今回の場合は以下のようになります。

<Suspense fallback={<Loading />}>
  <User /> 
</Suspense> 

fallbackの中にローディング中に表示したいコンポーネントを入れるだけです。 ローディング状態が命令的なものから宣言的なものになっています。
Suspense・SWRを取り入れることにより従来のコンポーネントの問題点を解決できることが理解できたと思います。

まとめ

コンポーネントの表示による違いと実装の違いを例にSuspense・SWRが何をしてくれるのかを説明してきました。
まとめると、SWRは今までコンポーネント内で行っていたデータの管理やフェッチの処理をuseSWRフックのみで完結させ、キャッシュまでも柔軟に管理してくれるライブラリです。
そして、Suspenseはローディング状態を宣言的にしてくれることにより、コンポーネント自体を非同期処理として扱え、責務を明確にするとともに、コンポーネント単位のロードを可能にしてくれました。

終わりに

ここまで読んでいただき、本当にありがとうございます。
間違っている点などがあれば、ぜひコメントでおしえてください!
この記事がSuspenseやSWRに興味を持つきっかけになってくれれば幸いです。

Copyright © RAKUS Co., Ltd. All rights reserved.