🎃

Redux Toolkit × RTK Query × GraphQLでToDoを試してみる

2021/06/22に公開約8,100字

これまでGraphQLでデータフェッチ及びキャッシュを扱う場合、Apollo Clienturqlが主な候補として挙がっていました。
しかしRedux Tookit v1.6.0でRTK Queryが組み込まれ、GraphQLを用いたデータフェッチも想定されています。Reduxを用いるような複雑な状態管理とパフォーマンスチューニングが必要になるケースで優秀な選択肢になりそうなので、ToDoアプリで使用感を試してみたいと思います。

参考

今回は一部公式のGraphQL Exampleを参考にしています。

https://redux-toolkit.js.org/rtk-query/usage/examples

事前準備

今回はGraphQLを使用するのでSchemaとモックサーバーを用意します。
タイトルのみを持つToDoを取得・作成できるシンプルなToDoです。
モックサーバーを用いる場合編集・完了・削除等の機能はほとんど作成機能のコードを複製する形になりがちなので、今回は割愛します。

schema.graphql
type ToDo {
  id: String!
  title: String!
}

type Query {
  toDos: [ToDo!]!
}

type Mutation {
  newToDo(title: String!): ToDo
}

apollo-serverでモックサーバーを立ち上げておきます。ToDoを試すだけのモックサーバーなのでcorsは'no-cors'を指定しておきます。

mock.ts
const { ApolloServer, makeExecutableSchema } = require('apollo-server')
const casual = require('casual')
const { importSchema } = require('graphql-import')

const typeDefs = importSchema('schema/schema.graphql');
const PORT = 4000;

const server = new ApolloServer({
  schema: makeExecutableSchema({
    typeDefs
  }),
  mocks: {
    Todo: () => ({
      id: casual.uuid,
      title: casual.title
    })
  },
  fetchOptions: {
    mode: 'no-cors',
  },
});

server.listen({ port: PORT }).then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

以上でSchemaとモックサーバーの準備は完了です。

フロントエンド

プロジェクトの作成

今回はToDoを試す目的なので、create-react-appのreduxテンプレートを使ってプロジェクトを立ち上げます。

npx create-react-app rtk-query-graphql-todo --template redux-typescript

APIリクエストコードを生成

まずはGraphQLリクエストを行うためのbaseQueryを作成します。
今回は公式のサンプル通りgraphql-requestを使用します。

yarn add graphql graphql-request

実際のプロダクトではこのBaseQueryを変更してauthorizationヘッダーなどを組み込んだり、色々カスタマイズすることになりそうです。

baseQuery.ts
import { BaseQueryFn } from "@reduxjs/toolkit/dist/query";
import { DocumentNode } from "graphql";
import { ClientError, request } from "graphql-request";

export const graphqlBaseQuery =
  ({
    baseUrl,
  }: {
    baseUrl: string;
  }): BaseQueryFn<
    { document: string | DocumentNode; variables?: any },
    unknown,
    ClientError
  > =>
  async ({ document, variables }) => {
    try {
      return { data: await request(baseUrl, document, variables) };
    } catch (error) {
      if (error instanceof ClientError) {
        return { error };
      }
      throw error;
    }
  };

BaseQueryが完成したので、ToDoの取得用queryを定義します。
gql関数を用いてqueryを定義し、getToDosのdocumentに渡します。

toDo.ts
import { createApi } from "@reduxjs/toolkit/query/react";
import { gql } from "graphql-request";

import { graphqlBaseQuery } from "./baseQuery";

const getToDosDocument = gql`
  query getToDos {
    toDos {
      id
      title
    }
  }
`;

export const toDoApi = createApi({
  reducerPath: "toDoApi",
  baseQuery: graphqlBaseQuery({ baseUrl: "http://localhost:4000" }),
  endpoints: (builder) => ({
    getToDos: builder.query({
      query: () => ({
        document: getToDosDocument,
      }),
    }),
  }),
});

export const { useGetToDosQuery } = toDoApi;

これでリクエスト用のCustomHookが生成されました。
ただし、この状態だと型情報がなくて不便なのでリクエストの前に型を生成をします。

型の生成

GraphQLのSchemaからTypeScript用の型を生成します。
graphql-codegenを使用するので、必要なライブラリを導入します。

yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations

graphql-codegenを見てみるとRTK Query用のコード生成も存在するようですが、今回はリクエストを試す目的なので使用を見送ります。

https://www.graphql-code-generator.com/docs/plugins/typescript-rtk-query

graphql-codegen用のconfigファイルを用意します。
今回はsrc/services/以下にAPIリクエスト用のファイルを作成しているので以下の設定になっています。
queryの型を生成するためにdocumentsには先程getToDosを定義したファイルを含めるようにしておきます。

graphql-codegen.yml
# 常に上書き
overwrite: true
# スキーマファイルを指定
schema:
  - /schema/*.graphql:
generates:
  src/services/types.ts:
    documents: "src/services/**.ts"
    plugins:
      - typescript
      - typescript-operations

設定が終わったので、コマンドを実行して型を生成します。
このコマンドは簡単に実行できるようにnpm scriptsに登録しておくと良いと思います。

yarn graphql-codegen --config ./graphql-codegen.yml

以上で型の生成が完了したので、先程のリクエストファイルに型を付けます。

src/api/toDo.ts
 import { createApi } from "@reduxjs/toolkit/query/react";
+import { GetToDosQuery } from "./types";
 import { gql } from "graphql-request";

 import { graphqlBaseQuery } from "./baseQuery";

 const getToDosDocument = gql`
   query getToDos {
     toDos {
       id
       title
     }
   }
 `;

 export const toDoApi = createApi({
   reducerPath: "toDoApi",
   baseQuery: graphqlBaseQuery({ baseUrl: "http://localhost:4000" }),
   endpoints: (builder) => ({
-    getToDos: builder.query({
+    getToDos: builder.query<GetToDosQuery, void>({
       query: () => ({
         document: getToDosDocument,
       }),
     }),
   }),
 });

 export const { useGetToDosQuery } = toDoApi;

これでレスポンスとqueryに渡す変数の型が付くようになりました。

storeの設定

storeの用意をします。
先程生成したtoDoApiをimportしてreducerに渡します。
storeの設定は以上で完了です。簡単ですね。

store.ts
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import { toDoApi } from "../services/toDo";

export const store = configureStore({
  reducer: {
    [toDoApi.reducerPath]: toDoApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(toDoApi.middleware),
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

リクエストの確認

後は生成されたカスタムフックを呼び出してAPIリクエストを行います。

app.tsx
import React from "react";
import { useGetToDosQuery } from "./services/toDo";

const App: React.FC = () => {
  const { data } = useGetToDosQuery();

  return (
    <div>
      ...
    </div>
  );
};

export default App;

ブラウザとRedux DevToolsで確認してみると、リクエストが成功していることと各種値がStoreに登録されていることがわかります。
フェッチ状態やエラーが自動で更新されるようになっているので、このあたりの記述をする手間が大きく省けます。


createToDo

ToDoの取得に成功したので次は作成機能を実装していきます。
やることは取得とほぼ同じで、toDoApiにcreateToDoを追加します

src/api/toDo.ts
 import { createApi } from "@reduxjs/toolkit/query/react";
+import {
+  CreateToDoMutation,
+  CreateToDoMutationVariables,
+  GetToDosQuery,
+} from "./types";
 import { gql } from "graphql-request";

 ~~~
 
+const createToDoDocument = gql`
+mutation createToDo($title: String!) {
+  newToDo(title: $title) {
+    id
+    title
+  }
+}
+`;

 export const toDoApi = createApi({
   reducerPath: "toDoApi",
   baseQuery: graphqlBaseQuery({ baseUrl: "http://localhost:4000" }),
   endpoints: (builder) => ({
     getToDos: builder.query<GetToDosQuery, void>({
       query: () => ({
         document: getToDosDocument,
       }),
+    createToDo: builder.mutation<
+      CreateToDoMutation,
+      CreateToDoMutationVariables
+    >({
+      query: ({ title }) => ({
+        document: createToDoDocument,
+        variables: {
+          title,
+        },
      }),
    }),
   }),
 });

-export const { useGetToDosQuery } = toDoApi;
+export const { useGetToDosQuery, useCreateToDoMutation } = toDoApi;

ToDo作成用のCustomHookが作成されたため、View側で呼び出しを行います。

app.tsx
import React from "react";
import { useGetToDosQuery, useCreateToDoMutation } from "./services/toDo";

const App: React.FC = () => {
  const { data } = useGetToDosQuery();
  const [createToDo] = useCreateToDoMutation();

  return (
    <div>
      ...
      <button
        onClick={() => {
          createToDo({ title });
        }}
      >
        Create Todo
      </button>
    </div>
  );
};

入力部分の状態管理は割愛しましたが、リクエストに関してはこれで完了です。
Redux DevToolsを確認するとStoreが更新されていることがわかります。

今回は実装しませんでしたが、RTK Queryには自動再フェッチや楽観的UIの機能等が提供されているため、このあたりの機能も簡単に実装できそうです。

https://redux-toolkit.js.org/rtk-query/usage/optimistic-updates

以上でRedux Toolkit × RTK Query × GraphQLでシンプルなToDoの実装ができました。

さいごに

Apollo等の既存のGraphQLのデータフェッチライブラリ単体では、中規模を超えるアプリで求められるような複雑な状態管理を実装するのは厳しい印象があります。
個人的にはそのあたりをRedux Toolkit単体でデータフェッチ〜状態管理までできるといいなぁと思っています。

RTK Queryはドキュメント・機能共に充実しているのも嬉しいところです。

GitHubで編集を提案

Discussion

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