43
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GraphQLAdvent Calendar 2018

Day 20

react-apolloでGraphQLを使いこなす

Last updated at Posted at 2018-12-08

はじめに

この記事は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)
  }
`;
43
37
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?