はじめに
今日も今日とて、フロントReact + バックRailsのSPA + APIのアプリ開発していたところ
TypeScriptのReact側で、react-apolloの型宣言がめんどくさいと思っていました。
バックエンド側はGraphQLを使用しているので、いろんなところに型宣言をしているようにも感じて、微妙。。
そこでgraphql-code-generatorを使っていろいろ気持ち悪い部分を解消していこうという話をします。
今回の構成
フロントエンド
- React(SPAで)
- TypeScript
- create-react-app
- React Apollo
バックエンド
- Ruby
- Rails(APIで)
- GraphQL
※上記2つのリポジトリはこちらのリポジトリから連動させる仕組みとしました。(開発環境として)
とりあえずApolloの公式通りにやってみる
バックエンド側にtodos
という、Todoモデルにあるデータを全て取得するAPIを作成しておきました。
これをフロントエンド側で取得し、表示します。
+import { gql, useQuery } from "@apollo/client";
import React from "react";
import logo from "./logo.svg";
import "./App.css";
+const TODOS_QUERY = gql`
+ query {
+ todos {
+ name
+ }
+ }
+`;
const App = () => {
+ const { loading, data } = useQuery(TODOS_QUERY);
+
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
- <a
- className="App-link"
- href="https://reactjs.org"
- target="_blank"
- rel="noopener noreferrer"
- >
- Learn React
- </a>
+ {loading ? (
+ <p>Loading ...</p>
+ ) : (
+ <ul>
+ {data && data.todos.map(({ name }, i) => <li key={i}>{name}</li>)}
+ </ul>
+ )}
</header>
</div>
);
};
export default App;
型宣言していないので、エラーが出ましたね。
型宣言してあげます。
用意する型は、Todo
モデルの型と、レスポンス値の型です。
レスポンスは{"data":{"todos": []}
という値が返るようにしています。
interface Todo {
name: string;
}
interface TodosData {
todos: Todo[];
}
TodosData
を以下のように使います。
- const { loading, data } = useQuery(TODOS_QUERY);
+ const { loading, data } = useQuery<TodosData>(TODOS_QUERY);
エラーなく実行できました。
では、GraphQL Code Generator
が入っていたらどうなるか試してみます。
GraphQL Code Generatorを使う
公式の手順通り、インストールとセットアップ
$ yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
"scripts": {
"generate": "graphql-codegen"
}
バックエンド側に用意しているエンドポイントはhttp://localhost:5000/graphql
なので、schema
にこれを使います。
schema: http://localhost:5000/graphql
documents: ./graphql/queries/*.graphql
generates:
./src/types.d.ts:
plugins:
- typescript
- typescript-operations
※documents
オプションを使用する場合に、@graphql-codegen/typescript-operations
が必要みたいです。
documents
に指定した場所に、todos
のクエリを記載します。
query {
todos {
name
}
}
バックエンドを起動してある状態で、用意したgenerate
コマンドを実行してみます。
$ yarn generate
src/types.d.ts
ファイルが生成されました。
...省略...
export type Unnamed_1_QueryVariables = Exact<{ [key: string]: never; }>;
export type Unnamed_1_Query = (
{ __typename?: 'Query' }
& { todos: Array<(
{ __typename?: 'Todo' }
& Pick<Todo, 'name'>
)> }
);
Unnamed
となってしまっているので、クエリに名前をつけて再度実行します。
-query {
+query todos {
todos {
name
}
}
src/types.d.ts
ファイルにTodosQuery
というType
が定義されました。
これをuseQuery
の型に利用してみます。
+ import { TodosQuery } from "./types.d";
- const { loading, data } = useQuery<TodosData>(TODOS_QUERY);
+ const { loading, data } = useQuery<TodosQuery>(TODOS_QUERY);
同じように動作が確認できました。
src/App.tsx
にTodo
モデルの型と、レスポンス値の型を定義しなくて良くなりました。
でも、TODOデータを取得する為のクエリをsrc/App.tsx
とgraphql/queries/todos.graphql
の2箇所に書いているのが気持ち悪いですよね。
todos
を取得する為の専用のuseQuery
があれば型もクエリも渡さなく済むのに。。。
@graphql-codegen/typescript-react-apolloを導入する
todos
を取得する為の専用のuseQuery
があれば型もクエリも渡さなく済むのに。。。
ということで、この気持ち悪いを解消していきます。
まずはインストール
$ yarn add -D @graphql-codegen/typescript-react-apollo
typescript-react-apollo
を追加します。
schema: http://localhost:5000/graphql
documents: ./graphql/queries/*.graphql
generates:
./src/types.d.ts:
plugins:
- typescript
- typescript-operations
+ - typescript-react-apollo
生成コマンドを実行
$ yarn generate
src/types.d.ts
にuseTodosQuery
という関数が生成されたので使ってみます。
- import { TodosQuery } from "./types.d";
+ import { useTodosQuery } from "./types.d";
- const { loading, data } = useQuery<TodosQuery>(TODOS_QUERY);
+ const { loading, data } = useTodosQuery();
ちゃんと動きましたね。
GraphQLの便利なところとして、同じAPIでも、必要なフィールドのみを取得することができる特徴があります。
先ほど、todos
のデータを取得する際、name
のみ指定し取得、一覧表示のような機能を実現しました。
これを個別に、編集、削除といった機能を実現するには、name
がユニークでない限り、id
のようなもので、
todo
を特定する必要があります。
別の画面等で、name
に加え、id
も必要な場面があった場合、以下のようなクエリを別で作成したくなってきます。
query todos {
todos {
id
name
}
}
しかし、graphql-code-generator
で生成する関数は全て、src/types.d.ts
に入るように設定しています。
ここには既に、name
のみを指定したtodos
を取得するクエリの関数が存在しているので、以下のようなファイルを作成し、
yarn generate
を実行すると、Not all operations have an unique name
というエラーが発生します。
query todos {
todos {
id
name
}
}
query
の右に記載している名前が、ユニークでないといけないってことですね。
-query todos {
+query todosIncludeId {
todos {
id
name
}
}
query
の名前をユニークな名前に変更してみました。
すると、src/types.d.ts
にuseTodosQuery
とは別に、useTodosIncludeIdQuery
関数が生成されました。
GraphQLの便利な特性を潰すことなく利用できますね。