Apollo ClientでのSuspense対応について

これは [GLOBIS Advent Calendar 2023](https://qiita.com/advent-calendar/2023/globis) 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を表示し、完了すると即座にレンダリングを開始します。

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


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

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

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

```jsx: 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してくれます。

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

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

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

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

```jsx: 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を組み合わせることで、よりスッキリ書けるようになります。

```jsx:Suspense導入前のLayoutComponent
const MainLayoutComponent: React.FC<MainLayoutProps> = ({
  header,
  footer = {},
  className,
}) => {
  return (
    <div>
      <Header {...header} />
      <main className={`${className}__main`}>
        <Outlet />
      </main>
      <Footer {...footer} />
    </div>
  )
}
```

```jsx: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`の指定をLayout１カ所で管理することも可能になります。
レイアウトを共有しながら、小要素であるcontainer部分だけ、loaderを表示させることが可能です。また、lazyを利用したコード分割時にも活きてきそうです。

```jsx: 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>
  )
}
```

```jsx: Suspense導入後のRoute定義
<Route element={<MainLayout />}>
  <Route path="/sample" element={<SuspenseSampleContainer />} />
  <Route path="/sample2" element={<SuspenseSampleContainer2 />} />
</Route>
```

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

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

https://recruiting-tech-globis.wraptas.site/