DEV Community

loading...

How to DRY Apollo GraphQL with QueryComponent

fuksito profile image Vitaliy Yanchuk Updated on ・2 min read

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)

pic
Editor guide
Collapse
zarabotaet profile image
Dima

Why u dont use hooks?