KA Engineering

KA Engineering

We're the engineers behind Khan Academy. We're building a free, world-class education for anyone, anywhere.

Subscribe

Latest posts

Creating Query Components with Apollo

Brian Genisio on June 12

Migrating to a Mobile Monorepo for React Native

Jared Forsyth on May 29

Memcached-Backed Content Infrastructure

Ben Kraft on May 15

Profiling App Engine Memcached

Ben Kraft on May 1

App Engine Flex Language Shootout

Amos Latteier on April 17

What's New in OSS at Khan Academy

Brian Genisio on April 3

Automating App Store Screenshots

Bryan Clark on March 27

It's Okay to Break Things: Reflections on Khan Academy's Healthy Hackathon

Kimerie Green on March 6

Interning at Khan Academy: from student to intern

Shadaj Laddad on Dec 12, 2016

Prototyping with Framer

Nick Breen on Oct 3, 2016

Evolving our content infrastructure

William Chargin on Sep 19, 2016

Building a Really, Really Small Android App

Charlie Marsh on Aug 22, 2016

A Case for Time Tracking: Data Driven Time-Management

Oliver Northwood on Aug 8, 2016

Time Management at Khan Academy

Several Authors on Jul 25, 2016

Hackathons Can Be Healthy

Tom Yedwab on Jul 11, 2016

Ensuring transaction-safety in Google App Engine

Craig Silverstein on Jun 27, 2016

The User Write Lock: an Alternative to Transactions for Google App Engine

Craig Silverstein on Jun 20, 2016

Khan Academy's Engineering Principles

Ben Kamens on Jun 6, 2016

Minimizing the length of regular expressions, in practice

Craig Silverstein on May 23, 2016

Introducing SwiftTweaks

Bryan Clark on May 9, 2016

The Autonomous Dumbledore

Evy Kassirer on Apr 25, 2016

Engineering career development at Khan Academy

Ben Eater on Apr 11, 2016

Inline CSS at Khan Academy: Aphrodite

Jamie Wong on Mar 29, 2016

Starting Android at Khan Academy

Ben Komalo on Feb 29, 2016

Automating Highly Similar Translations

Kevin Barabash on Feb 15, 2016

The weekly snippet-server: open-sourced

Craig Silverstein on Feb 1, 2016

Stories from our latest intern class

2015 Interns on Dec 21, 2015

Kanbanning the LearnStorm Dev Process

Kevin Dangoor on Dec 7, 2015

Forgo JS packaging? Not so fast

Craig Silverstein on Nov 23, 2015

Switching to Slack

Benjamin Pollack on Nov 9, 2015

Receiving feedback as an intern at Khan Academy

David Wang on Oct 26, 2015

Schrödinger's deploys no more: how we update translations

Chelsea Voss on Oct 12, 2015

i18nize-templates: Internationalization After the Fact

Craig Silverstein on Sep 28, 2015

Making thumbnails fast

William Chargin on Sep 14, 2015

Copy-pasting more than just text

Sam Lau on Aug 31, 2015

No cheating allowed!!

Phillip Lemons on Aug 17, 2015

Fun with slope fields, css and react

Marcos Ojeda on Aug 5, 2015

Khan Academy: a new employee's primer

Riley Shaw on Jul 20, 2015

How wooden puzzles can destroy dev teams

John Sullivan on Jul 6, 2015

Babel in Khan Academy's i18n Toolchain

Kevin Barabash on Jun 22, 2015

tota11y - an accessibility visualization toolkit

Jordan Scales on Jun 8, 2015

Meta

Creating Query Components with Apollo

by Brian Genisio on June 12

Here at Khan Academy, we've been using GraphQL in several projects with great success. GraphQL is a query language for describing the data you want from the backend API. It is an elegant solution to some of the problems that REST presents, including client performance.

Prevailing wisdom in the React/GraphQL space these days suggests that queries be co-located with the components which use them. We use Apollo as our client abstraction to the GraphQL endpoint such that co-located queries are relatively straightforward to structure, at least for simple cases.

As queries and component interactions become more complicated, however, the presentation and query layers become intertwined in a way that can feel overwhelming, especially from a separation of concerns perspective. Because of this, we've started to adopt a pattern we are calling "Query Components". I'll explain in more detail later in this post, but a Query Component is a component which abstracts the data definition from the Presentational Component, allowing the Presentational Component to be agnostic of Apollo or GraphQL.

Demo code

All of the demo code for this post can be found in in this Schools repo. You can play with it live if you'd like. There is not a ton of data, but it is an example of searching for schools by postal code. Some postal codes with data include 48103, 48104, and 48105.

It uses GraphCool as the backend GraphQL implementation. GraphCool is a really great way to experiment with GraphQL without learning how to implement a GraphQL server. It is a Backend as a Service (BaaS) for implementing a data store with a GraphQL interface.

A simple example

Let's start with a simple example of using Apollo to connect a GraphQL query to a React component. Here is a Presentational Component which displays a list of all the schools available and a "Loading" string while we are waiting. It is purposefully simple and shows how you can describe the data you want and display it with very little ceremony.

import React, {Component} from 'react';
import {graphql} from 'react-apollo';
import gql from 'graphql-tag';

class Schools extends Component {
    render() {
        if (this.props.data.loading) {
            return <div>Loading</div>;
        }

        return <ul>
        {
            this.props.data.allSchools.map((school) =>
                <li key={school.id}>{school.display}</li>
        )}
        </ul>;
    }
}

const SCHOOLS_QUERY = gql`query allSchools {
  allSchools(orderBy: display_ASC) {
    id
    postalCode
    display
  }
}`;

export default graphql(SCHOOLS_QUERY)(Schools);

A more complicated example

Unfortunately, simple examples like the one above are not the common case in practice. Let's see how this expands to be more like what you'd see in a real application.

  1. We'd like to pass a parameter to the component in order to filter the query by postal code.

  2. In addition to the data.loading flag, we'd like to know when there was an error and we'd like to know when the query was successful but the result was empty.

  3. We'd like to use Flow types, so we'll annotate all the types in this component.

  4. We'd like to include PropTypes in the component. Since we are using Flow we'll do so via Flow -> React PropTypes.

// @flow

import React, {Component} from 'react';
import {graphql} from 'react-apollo';
import gql from 'graphql-tag';

type School = {
    id: string,
    postalCode: string,
    display: string,
}

class Schools extends Component {
    props: {
        // Provided by the parent component
        postalCode: string,

        // provided by the Apollo wrapper
        schools: Array<School>,
        isLoading: boolean,
        isEmpty: boolean,
        isError: boolean,
    }

    render() {
        const {schools, isLoading, isEmpty, isError} = this.props;

        if (isLoading) {
            return <div>Loading</div>;
        }

        if (isEmpty) {
            return <div>No items match your postal code</div>;
        }

        if (isError) {
            return <div>There was an error processing your search</div>;
        }

        return <ul>{
            schools.map((school) =>
                <li key={school.id}>{school.display}</li>
            )
        }</ul>;
    }
}

const SCHOOLS_QUERY = gql`query allSchools($postalCode: String!) {
  allSchools(
      orderBy: display_ASC,
      filter: {postalCode: $postalCode}
  ) {
    id
    postalCode
    display
  }
}`;

type SchoolsData =  {
    loading: boolean,
    error: string,
    allSchools?: Array<School>,
};

const mapDataToProps = ({data}: {data: SchoolsData}) => {
    const isLoading = data.loading;
    const isEmpty = !!data.allSchools && data.allSchools.length === 0;
    const isError = !!data.error;
    const schools = data.allSchools || [];

    return {isLoading, isEmpty, isError, schools};
};

const mapPropsToOptions = ({postalCode}) => ({variables: {postalCode}});

export default graphql(SCHOOLS_QUERY, {
    options: mapPropsToOptions,
    props: mapDataToProps,
})(Schools);

Now that we've added these features, we have a new set of problems.

  1. The original code violates the "separation of concern" principle but this code is more egregious. This component is managing the GraphQL/Apollo integration and the presentation at the same time.

  2. The properties of the component don't properly communicate the intended usage. The parent needs to provide the postalCode property, but the other required properties (schools, isLoading, isEmpty, and isError) are provided by the mapDataToProps function.

  3. The actual presentation is tightly coupled to the query and glue logic. It is not uncommon for the same query to be used for a select component, or a table layout, or some other presentation. Providing a different presentation isn't immediately obvious.

Query Components

A pattern we've liked using lately is something we are calling "Query Components". Query Components encapsulate the query and Apollo glue logic into one component, which allows it to be composed with a separated Presentational Component.

It uses the "Functions as Children" pattern. For more information on this pattern, see Merrick Christensen's writeup in Function as Child Components.

SchoolsQuery

This is a Query Component which encapsulates all of the GraphQL query and the Apollo glue logic. It doesn't have any presentation.

// @flow
import gql from 'graphql-tag';
import {Component} from 'react';
import {graphql} from 'react-apollo';

export type School = {
    id: string,
    postalCode: string,
    display: string,
}

type SchoolQueryChildrenFn = (
    schools: Array<School>,
    details: {
        isLoading: boolean,
        isEmpty: boolean,
        isError: boolean,
    },
) => React$Element<any>;

class SchoolsQuery extends Component {
    props: {
        postalCode: string,
        children?: SchoolQueryChildrenFn,

        isLoading: boolean,
        isEmpty: boolean,
        isError: boolean,
        schools: Array<School>,
    }

    static defaultProps = {
        isLoading: false,
        isEmpty: false,
        isError: false,
        schools: [],
    }

    render() {
        const {isLoading, isEmpty, isError, schools, children} = this.props;

        return children && children(schools, {isLoading, isEmpty, isError});
    }
}

const SCHOOLS_QUERY = gql`query allSchools($postalCode: String!) {
  allSchools(
      orderBy: display_ASC,
      filter: {postalCode: $postalCode}
  ) {
    id
    postalCode
    display
  }
}`;

type SchoolsData =  {
    loading: boolean,
    error: string,
    allSchools?: Array<School>,
};

const mapDataToProps = ({data}: {data: SchoolsData}) => {
    const isLoading = data.loading;
    const isEmpty = !!data.allSchools && data.allSchools.length === 0;
    const isError = !!data.error;
    const schools = data.allSchools || [];

    return {isLoading, isEmpty, isError, schools};
};

const mapPropsToOptions = ({postalCode}) => ({variables: {postalCode}});

export default graphql(SCHOOLS_QUERY, {
    options: mapPropsToOptions,
    props: mapDataToProps,
})(SchoolsQuery);

Schools component

This is a Presentational Component which composes itself with the Query Component and the presentation. The interface is clear and the presentation is simple.

// @flow

import React, {Component} from "react";

import SchoolsQuery from "./SchoolsQuery.js";

class Schools extends Component {
    props: {
        postalCode: string,
    }

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

        return <SchoolsQuery postalCode={postalCode}>
            {(schools, {isLoading, isEmpty, isError}) => {
                if (isLoading) {
                    return <div>Loading</div>;
                }

                if (isEmpty) {
                    return <div>No items match your postal code</div>;
                }

                if (isError) {
                    return <div>There was an error processing your search</div>;
                }

                return <ul>{
                    schools.map((school) =>
                        <li key={school.id}>{school.display}</li>
                    )
                }</ul>;
            }}
        </SchoolsQuery>;
    }
}

export default Schools;

Going forward

Query Components have been a valuable pattern within our codebase, but they are new to us, and we are still evaluating how the pattern will scale over time. I expect an abstraction which codifies the pattern will be valuable. I'd also like to see how the same concept can be used for other API interfaces. The same approach would work for more traditional REST interfaces, for example. Whatever the case, I expect that we will be blogging more about GraphQL in the future.

Bonus!

This pattern can be applied to GraphQL mutations as well. By separating the mutation into a Mutation Component, the presentation only needs to activate the mutation. For the sake of brevity, I won't include the implementation here, but you can find it in the demo code.

You can compose the Mutation Component with the presentation like this:

<NewSchoolMutation
    postalCode={postalCode}
    display={display}
>
    {triggerMutation => <button onClick={triggerMutation}>
        Create!    
    </button>}
</NewSchoolMutation>