🐷

サーバー待ちのローディング表示をApollo Client + Reactで行う

2022/10/21に公開

導入

Apollo Client で構築したアプリケーションで、 GraphQL サーバーからの応答待ちの間に、いわゆる「スピナー」などのローディング表示を行いたい場合があります。

https://youtu.be/6ZIySVuxsCU

React Suspense は使わないの?

Apollo Clientはバージョン3.8でSuspenseに対応しました https://www.apollographql.com/docs/react/data/suspense

ローディング表示を行いたい場合、React 18 以降ではSuspenseが選択肢に上がるでしょう。ところが、GitHub: apollo-client - Adding React suspense + data fetching supportにあるように、Apollo Client 公式ではまだ Suspense との統合方法を紹介していません。

Apollo Client 公式が推奨する方法が決まるまでは、本記事のように Suspense を使わない従来のローディング表示が参考になると思います。

事前準備

チュートリアルで作成するアプリケーション全体の構成はこちらです。

apollo-client-loading-architecture

こちらの構成を作っていきましょう。

事前準備 1. 作業ディレクトリ

まずは本チュートリアル用のディレクトリを作成します。

以下のコマンドを実行してください
mkdir tutorial-apollo-client-loadin。
cd tutorial-apollo-client-loading

全体のディレクトリ構成は最終的に以下のようになります。

ディレクトリ構成
tutorial-apollo-client-loading
  +- server  # Apollo GraphQL Server
  +- client  # React Client

それでは次の手順で、上記のディレクトリ構成のうち、serverディレクトリから作成していきましょう。

事前準備 2. GraphQL サーバー

2 つのプロセス、Apollo Server と Server 側の GraphQL codegen を立ち上げます。

server procs

以下のコマンドを実行してください
# 一度に全部コピペで実行できます
mkdir server
cd server

# node.js setup
npm init -y
echo "node_modules" > .gitignore

TypeScript を導入して、ts-node-dev でサーバープロセスを立ち上げる準備をしましょう。

以下のコマンドを実行してください
# install and initialize typescript
npm install --save-dev typescript
npx tsc --init

# ts-node-dev: watch and restart a TypeScript server
npm install --save-dev ts-node-dev
npm pkg set scripts.start="ts-node-dev --watch src/*,data/*,./schema.gql --respawn src/index.ts"

Apollo Server と GraphQL Codegen を導入します。

以下のコマンドを実行してください
# apollo server
npm install apollo-server graphql

# install and setup graphql-codegen
npm install --save-dev @graphql-codegen/cli

# ここで npx graphql-code-generator init を行わず、後でcodegen.tsを生成。理由は後述。
npm install --save-dev \
  @graphql-codegen/typescript \
  @graphql-codegen/typescript-resolvers

npm pkg set scripts.generate="graphql-codegen --config codegen.yml --watch ./schema.gql,./data/*" # update generate script

@graphql-codegen/cliをインストールした後、通常はコマンドnpx graphql-code-generator initによってファイルcodegen.tsを生成しますが、そうすると対話モードに入ってしまい手入力が増えるのと、結局は生成された config.ts を上書き変更することになるので、以下のコマンドで config.ts を作成します。

以下のコマンドを実行してください
cat << EOF > codegen.ts;
import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  overwrite: true,
  schema: "schema.gql",
  generates: {
    "src/generated/graphql.ts": {
      plugins: ["typescript", "typescript-resolvers"],
      config: {
        avoidOptionals: true,
      },
    },
  },
  hooks: {
    afterOneFileWrite: ["npx prettier --write"],
  },
};

export default config;
EOF;

Apollo Server 立ち上げに必要な初期ソースコードを追加しましょう。

以下のコマンドを実行してください
# 開発ワークスペースのルートディレクトリに移動
# こうしないと、次のgit applyがエラーの理由も知らせず失敗する可能性あり
cd ../

curl https://github.com/richardimaoka/tutorial-apollo-client-loading/commit/65243e4ea244df1fd0bac1aeda9030643278e16c.patch \
  | git apply -v -

上記コマンドによる変更内容は以下のリンクのとおりです。

https://github.com/richardimaoka/tutorial-apollo-client-loading/commit/65243e4ea244df1fd0bac1aeda9030643278e16c

それでは、サーバーサイドの 2 つのプロセスを立ち上げましょう。

server procs

以下のコマンドを実行してください
cd server
npm run codegen # Serer GraphQL codegen
以下のコマンドを実行してください
cd server
npm run start # Apollo Server

2 つめのコマンドで以下のように GraphQL サーバーが立ち上がります。

実行結果
🚀  Server ready at: http://localhost:4000/

ブラウザから http://localhost:4000/ を開いて、Query your server ボタンを押して下さい。

apollo-server-ready

クエリを実行して、結果が返ってくれば成功です。

apollo-server

実行する GraphQL クエリはこちらです。

{
  employees {
    jobTitle
    name
    picturePath
  }
}

事前準備 3. React クライアント

ここからは、サーバー側の 2 つのプロセスに追加して、クライアント側プロセスを 2 つ立ち上げます。

four-processes

以下のコマンドを実行してください
npx create-react-app client --template typescript

cd client
npx prettier --write .
以下のコマンドを実行してください
# apollo client
npm install @apollo/client graphql

# install and setup graphql-codegen
npm install --save-dev @graphql-codegen/cli

# ここで npx graphql-code-generator init を行わず、後でcodegen.tsを生成。理由は後述。
npm install --save-dev \
  @graphql-codegen/typescript-operations \
  @graphql-codegen/typescript \
  @graphql-codegen/typescript-react-apollo

npm pkg set scripts.codegen="graphql-codegen --config codegen.ts --watch src/\\*_/_.tsx,../server/schema.gql"

@graphql-codegen/cliをインストールした後、通常はコマンドnpx graphql-code-generator initによってファイルcodegen.tsを生成しますが、そうすると対話モードに入ってしまい手入力が増えるのと、結局は生成された config.ts を上書き変更することになるので、以下のコマンドで config.ts を作成します。

以下のコマンドを実行してください
cat << EOF > codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  overwrite: true,
  schema: "../server/schema.gql",
  documents: "src/**/*.tsx",
  generates: {
    "src/generated/graphql.ts": {
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-react-apollo",
      ],
      config: {
        avoidOptionals: true,
      },
    },
  },
  hooks: {
    afterOneFileWrite: ["npx prettier --write"],
  },
};

export default config;
EOF

続いて、後ほどスピナーを表示するのに利用する FontAwesome も導入しましょう。公式ドキュメントの React 用セットアップ手順に従って以下を実行します。

以下のコマンドを実行してください
# Add SVG Core
npm i --save @fortawesome/fontawesome-svg-core

# Add Icon Packages
npm i --save @fortawesome/free-solid-svg-icons
npm i --save @fortawesome/free-regular-svg-icons

# Add the React Component
npm i --save @fortawesome/react-fontawesome@latest

React + Apollo Client 立ち上げに必要な初期ソースコードを追加しましょう。

以下のコマンドを実行してください
# 開発ワークスペースのルートディレクトリに移動
# こうしないと、次のgit applyがエラーの理由も知らせず失敗する可能性あり
cd ../

curl https://github.com/richardimaoka/tutorial-apollo-client-loading/commit/aba557674d984551b8533d5b5b6cdd10109d9ae7.patch \
  | git apply -v -

上記コマンドによる変更内容は以下のリンクのとおりです。

https://github.com/richardimaoka/tutorial-apollo-client-loading/commit/aba557674d984551b8533d5b5b6cdd10109d9ae7

それでは、クライアントサイドの 2 つのプロセスを立ち上げましょう。

four-processes

以下のコマンドを実行してください
cd client
npm run codegen # Client GraphQL codegen
以下のコマンドを実行してください
cd client
npm run start # React Apollo Client

ここで、ブラウザから https://localhost:3000 にアクセスして、以下のように表示されれば成功です。

faces

サーバー待ちがある場合のローディング表示

いよいよサーバー待ちのためを表現するための遅延を導入し、クライアント側でローディング表示を行います。

サーバーサイドに遅延を挿入

以下のコマンドを実行してください
# 開発ワークスペースのルートディレクトリに移動してから実行
curl https://github.com/richardimaoka/tutorial-apollo-client-loading/commit/4f982c7d0c6f4367835a52d5137e3694903f1f99.patch \
  | git apply -v -

上記のコマンドによるソースコードの変更はこちらの通りです

+ import { setTimeout } from "timers/promises";

...

search: async (_parent, _args, context, _info) => {
+  console.log("waiting started");
+  await setTimeout(3000, null);
+  console.log("waiting ended");
  return context.Query.search;
},
await setTimeout(3000,null) はなに?

https://nodejs.org/api/timers.html#timers_timers_promises_api を利用した sleep 処理です。この Node.js の Timers Promises API は Node.js の 16 から安定版になりました。

以前は Stack Overflow - How can I wait In Node.js (JavaScript)? l need to pause for a period of timeで記載されているように、以下のようなイディオムを書いて Sleep を表現することが通例でした。

await new Promise((resolve) => setTimeout(resolve, 5000));

これは「何もせず 5000 ミリ秒後に resolve して終わる Promise」であり、それを await で待っている処理です。慣れてしまえば、あるいは Promise や async/await に詳しければ意図を読み取れるものの、できれば以下のようにかけたほうがスッキリします。

await setTimeout(5000, null);

Timers Promises API ならこれが実現できます。

それではサーバー側の遅延を確認してみましょう。ボタンを押して 3 秒ほど待ってからクエリ結果が返ってきています。

https://www.youtube.com/watch?v=WhZ8lURGXP4

Apollo Server のログにはこのように表示されます。

🚀  Server ready at: http://localhost:4000/
waiting started
waiting ended

クライアントサイドにローディング表示を導入

以下のコマンドを実行してください
# 開発ワークスペースのルートディレクトリに移動してから実行
curl https://github.com/richardimaoka/tutorial-apollo-client-loading/commit/c599fc6b3b29e54196a659ebcfa0e6e38e004871.patch \
  | git apply -v -

上記のコマンドによるソースコードの変更はこちらの通りです。FontAwesome を利用したスピナーを追加しています。

+ import { faSpinner } from "@fortawesome/free-solid-svg-icons";
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

...

export const EmployeeListing = () => {
  const { loading, error, data } = useGetEmployeesQuery();
-  if (loading) return <>Loading...</>;
+  if (loading)
+    return (
+      <div>
+        <FontAwesomeIcon icon={faSpinner} size={"4x"} spin={true} />
+      </div>
+    );
  if (error) return <>error happened</>;
  if (!data || !data.employees) return <>empty data</>;

  ...

完成形はこちらのとおりです。無事 GraphQL サーバーからの応答を待つ間スピナーによるローディング表示を行う React コンポーネントが作動しました。

https://youtu.be/6ZIySVuxsCU

GitHubで編集を提案

Discussion