Edited at
GraphQLDay 20

react-apolloでGraphQLを使いこなす


はじめに

この記事はGraphQL Advent Calendar 2018の20日目です。

react-apolloを使ったフロントエンドの環境を作りはしたが、こういうコンポーネントはreact-apolloでどう書くんだ?という自分自身がハマったところをまとめてみました。


複数のapiを叩いた結果を表示するページが作りたい

こんな感じのページを作りたい場合

image.png

react-apolloのQueryを複数使えばできます。

import React, { Component } from 'react';

import { Query } from 'react-apollo';
import gql from 'graphql-tag';
import { CircularProgress } from '@material-ui/core';

const GET_USER = gql`
query($id: Int) {
user(id: $id) {
name
}
}
`
;

const GET_DELIVERY = gql`
query($userId: Int) {
delivery(userId: $userId) {
id
delivery_date
}
}
`
;

class Hoge extends Component<Props> {
props: Props;

render() {
return (
<div>
<Query query={GET_USER} variables={{ id: 1 }}>
{({ data, loading }) => {
const { user } = data;

if (loading || !user) {
return <CircularProgress />; // データのfetch中はスピナーがくるくる回る
}

return <div>{user.name}</div>;
}}
</Query>
<Query query={GET_DELIVERY} variables={{ userId: 1 }}>
{({ data, loading }) => {
const { delivery } = data;

if (loading || !delivery) {
return <CircularProgress />; // データのfetch中はスピナーがくるくる回る
}

return <div>{delivery.delivery_date}</div>;
}}
</Query>
</div>
);
}
}

export default Hoge;


無限スクロールするリストが作りたい

react-apolloのfetchMorereact-infinite-scrollerを組み合わせることでできます。

import React, { Component } from 'react';

import { Query } from 'react-apollo';
import InfiniteScroll from 'react-infinite-scroller';
import { CircularProgress } from '@material-ui/core';

const GET_ALL_COMPANIES = gql`
query($offset: Int, $limit: Int) {
allCompanies(offset: $offset, limit: $limit) {
data {
id
name
tel
representative_last_name
representative_first_name
post_code
prefecture
city
region
street
building
}
pageInfo {
startCursor
endCursor
hasNextPage
}
}
}
`
;

const CompanyListWithGql = () => {
return (
<Query
query={GET_ALL_COMPANIES}
variables={{
offset: 0,
limit: 100
}}
>
{({ data, fetchMore, loading }) => {
const { allCompanies } = data;

if (loading || !allCompanies) {
return <CircularProgress />;
}

return (
<CompanyList allCompanies={allCompanies} fetchMore={fetchMore} />
);
}}
</Query>
);
};

class CompanyList extends Component<Props> {
props: Props;

onLoadMore = () => {
const {
allCompanies: { data: companies },
fetchMore
} = this.props;

fetchMore({
variables: {
offset: companies.length
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;

const prevCompanies = prev.allCompanies.data;
const currentCompanies = fetchMoreResult.allCompanies.data;

return {
...prev,
allCompanies: {
...prev.allCompanies,
data: [...prevCompanies, ...currentCompanies],
pageInfo: fetchMoreResult.allCompanies.pageInfo
}
};
}
});
};

render() {
const {
allCompanies: { data: companies, pageInfo }
} = this.props;

return (
<InfiniteScroll
loadMore={this.onLoadMore}
hasMore={pageInfo.hasNextPage}
loader={<CircularProgress />}
>
<table className="table is-striped">
<thead>
<tr>
<th>ID</th>
<th>会社名</th>
<th>tel</th>
<th>郵便番号</th>
<th>都道府県</th>
<th>市区町村</th>
<th>地域名</th>
<th>番地</th>
<th>建物名</th>
</tr>
</thead>
<tbody>
{companies.map((company, i) => (
<tr key={company.id}>
<td>{company.id}</td>
<td>{company.name}</td>
<td>{company.tel}</td>
<td>{company.post_code}</td>
<td>{company.prefecture}</td>
<td>{company.city}</td>
<td>{company.region}</td>
<td>{company.street}</td>
<td>{company.building}</td>
</tr>
))}
</tbody>
</table>
</InfiniteScroll>
);
}
}

export default CompanyListWithGql;

上記の例だと、初期表示はoffset: 0, limit: 100でデータを取ってきて、100件表示するごとにoffsetの値が+100されます。

react-infinite-scrollerでは以下のようにloaderを指定することでapiのfetch中に表示するコンポーネントを指定できるので、100件目まで表示する => スピナーがくるくる回る => 次の100件が追加表示される、、、という無限スクロールができます。

      <InfiniteScroll

loadMore={this.onLoadMore}
hasMore={pageInfo.hasNextPage}
loader={<CircularProgress />}
>

また、この無限スクロールを実現するためにはapollo server側では以下の値を返すように実装する必要があります。


const GET_ALL_COMPANIES = gql`
query($offset: Int, $limit: Int) {
allCompanies(offset: $offset, limit: $limit) {
data { // dataには会社情報
id
name
tel
post_code
prefecture
city
region
street
building
}
pageInfo { // pageInfoには無限スクロールのための情報を含める
startCursor // offsetの値
endCursor // limitの値
hasNextPage // データがまだあるかどうかのboolean
}
}
}
`
;

上記のレスポンスを返すapollo server側の実装例です。ORMにsequelizeを使っています。


const companies = await db.companies.findAll({
attributes: [
'id',
'name',
'tel',
'post_code',
'prefecture',
'city',
'region',
'street',
'building',
],
offset,
limit,
});

const count = await db.companies.count();

return {
data: companies,
pageInfo: {
startCursor: offset,
endCursor: limit,
hasNextPage: offset !== count,
},
};


フォームが作りたい

バリデーションにformikを使ったフォームを作るとしたらこんなイメージ

import React, { Component } from 'react';

import { withFormik, Field, Form } from 'formik';
import * as yup from 'yup';
import { graphql, compose } from 'react-apollo';
import gql from 'graphql-tag';

export const UPDATE_USER = gql`
mutation updateUser($name: String, $age: Int) {
updateUser(name: $name, age: $age) {
id
name
age
}
}
`
;

class Hoge extends Component<Props> {
props: Props;

render() {
const { isSubmitting } = this.props;

return (
<Form>
<div>
<div>
<label>
名前
<Field type="text" placeholder="名前" name="name" />
</label>
</div>
<div>
<label>
年齢
<Field type="number" placeholder="年齢" name="age" />
</label>
</div>
<div>
<button type="submit" disabled={isSubmitting}>
更新
</button>
</div>
</div>
</Form>
);
}
}

export default compose(
graphql(UPDATE_USER, { name: 'updateUser' }), // 1) nameで指定した値で
withFormik({
mapPropsToValues: ({ user }) => ({
name: user.name,
age: user.age
}),
handleSubmit: (values, { props }) => {
const { updateUser } = props; // 2) propsに関数として渡ってくる

updateUser({
variables: {
name: values.name,
age: values.age
}
});
},
validationSchema: yup.object().shape({
name: yup.string().required(),
age: yup.number().required()
})
})
)(Hoge);

react-apolloのcomposeを使ってgraphqlの関数をコンポーネントに合成する感じです。


export default compose(
graphql(UPDATE_USER, { name: 'updateUser' }),

こうすることでgraphqlのUPDATE_USERを実行できる関数がreactのコンポーネントにprops.updateUserとして渡って来ます。ここでnameを指定しないと、props.mutateという関数で渡ってくるのでnameは指定した方がいいでしょう。

また、composeを使って合成するgraphqlの関数の数ですが、こう書くことで複数の関数をpropsとして渡せます。

参考url: https://www.apollographql.com/docs/react/basics/setup

export default compose(

graphql(gql`mutation (...) { ... }`, { name: 'createTodo' }),
graphql(gql`mutation (...) { ... }`, { name: 'updateTodo' }),
graphql(gql`mutation (...) { ... }`, { name: 'deleteTodo' }),
)(MyComponent);

function MyComponent(props) {
// Instead of the default prop name, `mutate`,
// we have three different prop names.
console.log(props.createTodo);
console.log(props.updateTodo);
console.log(props.deleteTodo);

return null;
}


フォームで値を更新した際、表示されてる値が巻き戻る

以下のような事象に遭遇した際に行った対処方法です。

こういうユーザーの情報を更新できるフォームがあるとします。

image.png

ユーザーの名前を更新して、更新ボタンを押すと、、

image.png

データは更新されているが、表示上は元に戻っている、という事象が起こりました。

image.png

ここに対処方法が載っています。

https://www.apollographql.com/docs/react/advanced/caching.html#automatic-updates

原因としては正しくキャッシュが更新されていなかったことが原因でした。

事象発生時はユーザー更新のmutationの返り値がidしか取得していない状態でした。

export const UPDATE_USER = gql`

mutation updateUser($name: String, $age: Int) {
updateUser(name: $name, age: $age) {
id
}
}
`
;

これをname,ageも取得するようにしたところ、キャッシュも更新されるようになりフォームの表示が巻き戻るという事象も解消しました。


export const UPDATE_USER = gql`
mutation updateUser($name: String, $age: Int) {
updateUser(name: $name, age: $age) {
id
name
age
}
}
`
;

また、react-apolloのクエリを実行する際にはfetchPolicyを指定することでキャッシュを使うか使わないかを選ぶことができます。

そもそもキャッシュを使わないようにすることでも、この事象を解決することができましたが、つどfetchすることになるため表示は遅くなりました。


reactのライフサイクルやonClickとかでgraphqlのクエリが叩きたい

withApolloをcomposeを使ってコンポーネントと合成すると、propsにgraphqlのclientが渡って来ます。

そのclientを使ってgraphqlのquery、mutationが叩けます。

import React, { Component } from 'react';

import { compose, withApollo } from 'react-apollo';
import gql from 'graphql-tag';

const GET_USER = gql`
query($id: Int) {
user(id: $id) {
name
}
}
`
;

class Hoge extends Component<Props> {
props: Props;

componentDidMount = async () => {
const { client } = this.props;
const { data } = await client.query({
query: GET_USER,
variables: { id: 1 }
});

// data.userを使ってなんか処理する
};

render() {
return <div>some code</div>;
}
}

export default compose(withApollo)(Hoge);


propsにgraphqlの関数を渡す二つの方法

コンポーネントのpropsにgraphqlの関数を渡す方法は以下のふた通りがあります。

// graphqlを使う方法

export default compose(graphql(UPDATE_USER, { name: 'updateUser' }))(MyComponent),

// withApolloを使う方法

export default compose(withApollo)(MyComponent);

しかし、graphqlをcomposeする方法の場合、queryのgraphql関数がis not a functionのエラーになるバグ?にぶち当たりました。(mutationだとなぜか大丈夫)

https://stackoverflow.com/questions/48327994/apollo-react-compose-query-is-not-a-function

このエラーに遭遇した場合はwithApolloを使えばいいと思います。


mutationに渡す引数をオブジェクトにしたい

以下のupdateUserというmutationですが、更新したい属性が増えた場合、引数をどんどん増やしていくことでも対応できますが、記述が冗長になっていきます。

// これくらいならまだいいが、、

export const UPDATE_USER = gql`
mutation updateUser($name: String, $age: Int) {
updateUser(name: $name, age: $age) {
id
name
age
}
}
`
;

// 引数が多くなってくるとコードが見にくい!

export const UPDATE_USER = gql`
mutation updateUser($name: String, $age: Int, $hoge: string, $huga: string, $bar: string) {
updateUser(name: $name, age: $age, hoge: $hoge, huga: $huga, bar: $bar) {
id
name
age
hoge
huga
bar
}
}
`
;

この場合、apollo-server側でschemaを定義することで記述をスッキリさせることができます。

mutationに渡す引数のスキーマの定義にはinput typesを使います。

以下のような感じでinputのスキーマを定義しておくと、、、

  input UpdateUserParams {

name: String!
age: Int!
hoge: String
huga: String
bar: String
}

// !をつけることでその属性は必須のパラメータにできます。

先ほどのupdateUserはこのように書くことができます。

export const UPDATE_USER = gql`

mutation updateUser($input: UpdateUserParams!) {
updateUser(input: $input) {
id
name
age
hoge
huga
bar
}
}
`
;


返り値を必要としないmutationが書きたい

mutationのクエリを実行した際、特に返り値を必要としない場合は以下のようにかけます。

export const UPDATE_USER = gql`

mutation updateUser($input: UpdateUserParams!) {
updateUser(input: $input)
}
`
;