👌

Apollo Client で Suspense を使ってみた

2023/01/19に公開約3,400字

Suspense とは

React18から正式に導入された機能です。

データフェッチのあるコンポーネントをSuspenseコンポーネントで囲むことでローディングの表示などを制御できます。
コンポーネント内でデータ取得の処理中に Promise を throw する必要があります。

https://ja.reactjs.org/blog/2022/03/29/react-v18.html#new-suspense-features

詳細な仕様はこちらにあります。
https://github.com/reactjs/rfcs/blob/main/text/0213-suspense-in-react-18.md

Apollo Client で使うには

Suspense を使うには Promise を throw する必要があり、クライアントライブラリがそれに対応している必要があります。

relay や urql などの他の GraphQL クライアントは Suspense 対応していますが、 Apollo ではまだ正式に使える状態になっていません。

ただ α版 として開発されているバージョンがありそれを使うことで Apollo でも Suspense を使うことができるようになります。

# @apollo/client にはまだ Suspense の機能が入っていない
npm install @apollo/client@3.8.0-alpha.0 graphql

使ってみる

Suspense を使わない場合

下記のように Provider を設定します。

import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://hoge.com',
  cache: new InMemoryCache(),
});

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <ApolloProvider client={client} suspenseCache={cache}>
      <App />
    </ApolloProvider>
  </React.StrictMode>
);

実際に使用するコンポーネントでは下記のようにGraphqlを叩けます。
AllListコンポーネントの中でローディングとエラーの状態管理をしています。

import { gql, useQuery  } from '@apollo/client';

const GET_ALL = gql`
  query {
    all {
      name
    }
  }
`;

const AllList = () => {
  const { loading, data, error } = useQuery(GET_ALL);

  if (loading) {
    return <p>loading</p>
  }

  if (error) {
    return <p>{error.message}</p>
  }

  return (
    <>
      {data.all.map((value: any) => { return (<li>{value.name}</li>)})}
    </>
  )
}

const App = () =>  {
  return (
    <AllList />
  );
}

export default App;

Suspense を使う場合

下記のように Provider を設定します。新たに SuspenseCache の設定が必要になります。

import { ApolloClient, InMemoryCache, ApolloProvider, SuspenseCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://hoge.com',
  cache: new InMemoryCache(),
});

const cache = new SuspenseCache()

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <ApolloProvider client={client} suspenseCache={cache}>
      <App />
    </ApolloProvider>
  </React.StrictMode>
);

useSQuery の代わりに useSuspenseQuery を使います。

AllListコンポーネントの中ではデータフェッチと表示のコードだけを書いており、ローディングの管理はSuspenseコンポーネントが行うような形になっています。

ではエラーの管理はどうするかというと公式にはクラスコンポーネントでErrorBoundaryというコンポーネントを用意してエラーをキャッチするように書いています。
https://ja.reactjs.org/docs/error-boundaries.html

ただ、クラスコンポーネントを書きたくないなぁと思ってライブラリを探していたところreact-error-boundaryという良さげなライブラリを見つけたのでそれを使いました。
https://github.com/bvaughn/react-error-boundary

この書き方で

  • データの取得と表示
  • ローディングの処理
  • エラーの処理
    という三つの責務をコンポーネント毎に分けて書けるようになりました。
import { Suspense } from 'react';
import { gql, useSuspenseQuery_experimental as useSuspenseQuery  } from '@apollo/client';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';

const GET_ALL = gql`
  query {
    all {
      name
    }
  }
`;

const ErrorComponent = ({error}: FallbackProps) => {
  return (
    <p>{error.message}</p>
  )
}

const AllList = () => {
  const { data } = useSuspenseQuery(GET_ALL);

  return (
    <>
      {data.all.map((value: any) => { return (<li>{value.name}</li>)})}
    </>
  )
}

const App = () => {
  return (
    <ErrorBoundary FallbackComponent={ErrorComponent}>
      <Suspense fallback={<p>loading</p>}>
        <AllList />
      </Suspense>
    </ErrorBoundary>
  );
}

export default App;

Discussion

ログインするとコメントできます