DEV Community

Vitaliy Yanchuk
Vitaliy Yanchuk

Posted on • Updated on

How to DRY Apollo GraphQL with QueryComponent

An official Apollo React guide illustrates an example on how to load a query and show data this way:

import gql from "graphql-tag";
import { Query } from "react-apollo";

const GET_DOGS = gql`
  {
    dogs {
      id
      breed
    }
  }
`;

const Dogs = () => (
  <Query query={GET_DOGS}>
    {({ loading, error, data }) => {
      if (loading) return "Loading...";
      if (error) return `Error! ${error.message}`;

      return (
        <select name="dog" onChange={onDogSelected}>
          {data.dogs.map(dog => (
            <option key={dog.id} value={dog.breed}>
              {dog.breed}
            </option>
          ))}
        </select>
      );
    }}
  </Query>
);

This is nice for the start, but I don't want to copy over all the handling of errors each time in each usage of Query, also I want to write more interesting loader that just text, so having this duplicated code in all the places seems as boilerplate.

So I DRY it with a QueryComponent, so the above example would become:

class Dogs extends QueryComponent {
  query(){
    return `
      {
        dogs {
          id
          breed
        }
      }    
    `
  }

  content(){
    const data = this.state.data
    return (
      <select name="dog">
        {data.dogs.map(dog => (
          <option key={dog.id} value={dog.breed}>
            {dog.breed}
          </option>
        ))}
      </select>
    );    
  }
}

Now I can configure nice loader, and have it same across the site, and make handling errors centralized, and I don't need to import gql every time.

Code for QueryCompoment would be such:

import React from "react";
import PropTypes from "prop-types";
import gql from "graphql-tag";

import Loader from "./Loader";

class QueryComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { loading: true, error: null };
  }

  componentDidMount() {
    this.loadQuery({ variables: this.queryVariables() });
  }

  query() {
    throw "query() should be redefined on extended component";
  }

  queryVariables() {
    return {};
  }

  loadQuery({ variables }) {
    const client = this.context.client;
    const query = gql(this.query());

    try {
      client
        .watchQuery({ query, variables }) // performs query as well
        .subscribe(
          ({ data, loading, error, errors, networkStatus }) => {
            // window.console.log("query subscribe fired")
            if (loading) return;
            if (error) {
              this.queryFailed({ error, errors, networkStatus });
            } else {
              this.queryLoaded(data);
            }

            if (this.state.loading) {
              this.setState({ loading: false });
            }
          },
          error => {
            console.log("Query Failed");
            console.dir(error);
            this.queryFailed({ error });
          }
        );
    } catch (error) {
      console.log("query error");
      console.log(error);
      this.setState({
        loading: false,
        error
      });
    }
  }

  loading() {
    return <Loader />;
  }

  queryLoaded(data) {
    this.setState(data);
  }

  queryFailed({ error }) {
    this.setState({
      loading: false,
      error
    });
  }

  handleError(error) {
    const message = error.message;
    return <div>{message}</div>;
  }

  render() {
    const { loading, error } = this.state;
    if (loading) return this.loading();
    if (error) return this.handleError(error);
    return this.content();
  }
}

QueryComponent.contextTypes = {
  client: PropTypes.object
};

export default QueryComponent;


I simplified code a bit for the sake of clarity.

You can check more advanced working example, that also passed query variables, in CodeSandbox:

Discussion (1)

Collapse
zarabotaet profile image
Dima

Why u dont use hooks?