使用 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的fetchMore和react-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获取中显示的组件,因此可以实现无限滚动,直到显示第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的实现示例。我们使用Sequelize作为ORM。


  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,
    },
  };

我想要制作一个表格。

(Wǒ .)

如果使用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函数作为props.updateUser传递到React组件中。如果没有指定name,则会通过props.mutate函数传递,所以最好指定name。

此外,使用compose合成GraphQL函数的数量是如此,通过这样的写法可以将多个函数作为props传递。

参考链接: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
    }
  }
`;

另外,我们可以通过指定fetchPolicy来选择是否使用缓存来执行react-apollo的查询。
虽然最开始没有使用缓存就能解决这个问题,但由于每次都需要进行fetch操作,导致显示变得较慢。

我想在React的生命周期或者onClick事件中使用GraphQL查询。

使用compose将withApollo与组件合成后,能够将graphql的client作为props传递给组件。
通过使用该client,可以发起graphql的查询和变更操作。

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);

然而,当使用compose方法来组合graphql时,遇到了query的graphql函数出现了”is not a function”错误的bug?(而对于mutation则没有这个问题)

如果遇到这个错误,我认为可以使用”withApollo”来解决。

我想将传递给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的参数的schema时,可以使用input types。

如果在输入时定义了类似以下的模式,那么…

  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)。

当执行变异查询时,如果不需要特定的返回值,则可以按照以下方式编写。

export const UPDATE_USER = gql`
  mutation updateUser($input: UpdateUserParams!) {
    updateUser(input: $input)
  }
`;
广告
将在 10 秒后关闭
bannerAds