LoginSignup
6
0

Apollo ClientでのSuspense対応について

Last updated at Posted at 2023-12-09

これは GLOBIS Advent Calendar 2023 10日目の記事です。

はじめに

こんにちは。GLOBIS学び放題でWebエンジニアをしている富山です。

GLOBIS学び放題のWebフロントエンドの構成は、React(TypeScript) / GraphQLで、GraphQLのクライアントライブラリには、Apollo Client + GraphQL Code Generatorを利用しています。

今回は、Apollo Clientのバージョンアップにより、Suspenseが利用可能になったので、本記事では、実際に学び放題のコードベースに適応しようとするとどうなるか検証してみました。

Apollo Client v3.8からSuspense対応された

Suspense自体はReact18でリリースされたもので、Reactを利用してる方ならお馴染みの機能ですが、Relayやurql等、他のGraphQL APIのクライアントライブラリがSuspenseに対応してる中、Apollo Clientはやや周回遅れ気味でしたが、遂にApollo Clientもv3.8からSuspense対応されました!

Suspenseを導入するにあたって

ざっくりSuspenseについておさらいです。

Suspenseは、非同期処理を管理する機能で、対象のコンポーネントがフェッチデータを持っていることを伝えるための仕組みになります。

データフェッチ完了後にレンダリングしたいコンポーネントは Suspenseでwrapすると、小要素が読み込みを完了するまで、最も近いSuspenseからfallbackに渡したReactNodeを表示し、完了すると即座にレンダリングを開始します。

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

Apollo Client環境にSuspenseを導入する

Apollo Clientが使われてるプロジェクトでは、以下のような処理を見たり、書いたりしたことがあると思います。

data型は T | undefined なので値のチェックをしてから使用します。

学び放題のデータフェッチは、各ページのcontainer層でデータをfetchして、pages層 → component層に流していくので、各ページのcontainerには以下のような定型コードがあります。useQueryから返ってくるloadingerrorで制御しており、dataはundefinedにもなり得るので、値のチェックもします。

Suspense 導入前のコード
export const SampleContainer = () => {

  const {
    data: samplePageData,
    loading: samplePageLoading,
    error: samplePageError,
  } = useSamplePageQuery()

  if (samplePageError) {
    throw samplePageError
  }

  if (!sampleData || samplePageLoading) {
    return <LoadingScreen />
  }
}

Apollo Client3.8以降では、useSuspenseQueryというhooksが追加されています。

このhooksは、文字通りuseQueryのSuspense対応版で、リクエストが行われている間は、呼び出し元のコンポーネントをsuspendしてくれます。

以下、サンプルのコードですが、loadingerrorの状態を外で管理できるようになりました。

これで、dataの型が T | undefinedでなくなるので、dataのundefinedのチェックが不要になります。

また、エラーハンドリングは、これまで、useQueryの返り値を使用していましたが、ErrorBoundaryでラップして制御する形が推奨されています。(useSuspenseQueryerrorPolicyを指定することで、これまで通りにerrorの返り値を使った実装も可能)

コンポーネントの内部にあったloadingerrorの状態が外部で扱えるようになり、これまでloadingerrorの状態を見ていたファイルは、dataのみを扱えば良くなるので、見通しが良くなります。

Suspense導入後のコード
export const SuspenseSampleContainer = () => {
  const { data: sampleSuspensePageData } = useSampleSuspensePageSuspenseQuery()

  return <SuspenseSamplePage {...sampleSuspensePageData} />
}

export const AppRoute: React.FC<Props> = () => {
  return (
    <Route
      path="/sample"
      element={
        <ErrorBoundary fallback={<>Something went wrong</>}>
          <Suspense fallback="loading...">
            <SuspenseSampleContainer />
          </Suspense>
        </ErrorBoundary>
      }
    />
  )
}

OutletとSuspenseを併用する

また、OutletとSuspenseを組み合わせた利用も相性が良さそうです。

Globis学び放題では、React-Router v6から利用できるようになったOutletを活用してレイアウトを共有しながら、中身の各ページのみをルーティングできるようにしています。Outletのあるコンポーネントを親のRouteに設定すると、子のRouteに定義しているコンポーネントは、Outletを置き換える形で表示されます。OutletとSuspsenを組み合わせることで、よりスッキリ書けるようになります。

Suspense導入前のLayoutComponent
const MainLayoutComponent: React.FC<MainLayoutProps> = ({
  header,
  footer = {},
  className,
}) => {
  return (
    <div>
      <Header {...header} />
      <main className={`${className}__main`}>
        <Outlet />
      </main>
      <Footer {...footer} />
    </div>
  )
}
Suspense導入前のRoute定義
<Route
  element={
    <MainLayout />
  }
>
  <Route
    path="/sample"
    element={
      <Suspense fallback="loading...">
        <SuspenseSampleContainer />
      </Suspense>
    }
  />

  <Route
    path="/sample2"
    element={
      <Suspense fallback="loading">
        <SuspenseSampleContainer2 />
      </Suspense>
    }
  />
</Route>

OutletをSuspnseで囲むことで、fallbackの指定をLayout1カ所で管理することも可能になります。
レイアウトを共有しながら、小要素であるcontainer部分だけ、loaderを表示させることが可能です。また、lazyを利用したコード分割時にも活きてきそうです。

Suspense導入後のLayoutComponent
const MainLayoutComponent: React.FC<MainLayoutProps> = ({
  header,
  footer = {},
  className,
}) => {
  return (
    <div>
      <Header {...header} />
      <main className={`${className}__main`}>
        <Suspense fallback="loading...">
          <Outlet />
        </Suspense>
      </main>
      <Footer {...footer} />
    </div>
  )
}
Suspense導入後のRoute定義
<Route element={<MainLayout />}>
  <Route path="/sample" element={<SuspenseSampleContainer />} />
  <Route path="/sample2" element={<SuspenseSampleContainer2 />} />
</Route>

最後に

Apollo Clientv3.8以降では、Suspenseの他にもFragment Colocationの導入もしやすくなっていたりと、色々と進化してる部分があり、ドキュメントを読むのも楽しかったです。

グロービスの開発組織では、一緒にモノづくりに取り組んでくれる仲間を募集しています!興味がある方はぜひご連絡ください!!

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0