June 21, 2017

5 things they don’t want you to know about React-Apollo

Kurt Kemple

Kurt Kemple

This is a guest post from Kurtis Kemple, tech lead on the UI team @MLS.

All jokes aside, <a href="http://dev.apollodata.com/react/" target="_blank" rel="noreferrer noopener">react-apollo</a> is an amazing tool that allows you to solve some really hard data fetching problems with very minimal code. It integrates very easily with existing and greenfield apps and supports Redux integration. If you want to use GraphQL, it’s a tool that I highly recommend investigating.

Over the last few months at Major League Soccer, we have been working a lot with GraphQL/Apollo and have discovered some really interesting tricks (and things that aren’t exactly “tricks”, but are still pretty awesome) you can use to solve some of the more common data fetching/handling issues in building highly dynamic applications. Here are our top five, along with code examples showing how they work.

This is not an “intro to react-apollo” example, so if you’re unfamiliar with the project I highly recommend reading the docs first!

1.Dealing with Pagination

A problem that most developers face is fetching paginated data. React-apollo makes this a pretty trivial thing with the fetchMore method off of the data prop. This method allows you to repeat the same query with new variables and then alter the original response, adding the new results to the old.

1import React, { Component } from 'react';2import { graphql } from 'react-apollo';3import { gql } from 'graphql-tools';45const QUERY = gql`6  query MatchesByDate($date: Int) {7    matches(date: $date) {8      id9      minute10      period11      highlights {12        ...13      }14      ...15    }16  }17`;1819class MatchList extends Component {      20  render() {21    const ({ data: { loading, errors, matches } }) = this.props;2223    return (24      <div className="matchlist">25        {loading && <div className="loader" />}26        {errors && <div className="errors">...</div>}2728        {!loading && !matches && <span className="none">No Matches Found!</span>}2930        {!loading &&31          matches &&32          matches.map(match => <div className="match">...</div>)}3334        {!loading &&35          matches &&36          <button onClick={this.onFetchMore}> 37            Fetch More Matches38          </button>}39      </div>40    );41  }42  43  onFetchMore = () => {44    const ({ data: { matches, fetchMore } }) = this.props;4546    fetchMore({47      variables: { date: matches[matches.length - 1].date },48      updateQuery: (previousResult, { fetchMoreResult, queryVariables }) => {49        return {50          ...previousResult,51          // Add the new matches data to the end of the old matches data.52          matches: [53            ...previousResult.matches,54            ...fetchMoreResult.matches,55          ],56        };57      },58    });59  }60}6162export default graphql(QUERY)(MatchList);

2. Conditional Data Fetching

There may be times when you only want to fetch data if some requirements are met — for example, one case might be offering more functionality for desktop users. We can accomplish things like this by using the <a rel="noreferrer noopener" href="http://dev.apollodata.com/react/higher-order-components.html#compose" target="_blank">compose</a> higher order component that react-apollo provides in conjunction with the skip config property. The skip property allows you to tell react-apollo to skip queries based on certain parameters (in most cases based on the props passed to the component).

1import React from 'react';2import { compose, graphql } from 'react-apollo';3import { gql } from 'graphql-tools';45import MatchSummary from '...';6import MatchHighlights from '...';78const MATCH_SUMMARY_QUERY = gql`9  query MatchSummary($id: Int) {10    match(id: $id) {11      minute12      period13      home {14        score15      }16      away { 17        score18      }19    }20  }21`;2223const MATCH_HIGHLIGHTS_QUERY = gql`24  query MatchHighlights($id: Int) {25    match(id: $id) {26      highlights {27        ...28      }29    }30  }31`;3233const Match = ({ media, data: { loading, errors, match } }) =>34  <div className="matchlist">35    {loading && <div className="loader" />}36    {errors && <div className="errors">...</div>}37    {!loading && <MatchSummary match={match} />}38     39    {/* if on desktop render highlights */}40    {!loading && media === 'desktop' && <MatchHighlights match={match} />}41  </div>;4243export default compose(44  graphql(MATCH_SUMMARY_QUERY),45  graphql(MATCH_HIGHLIGHTS_QUERY, {46    // skip passes props if given a function47    skip: ({ media }) => media !== 'desktop',48  }),49)(Match);

3. Batching Requests

Another common problem faced when building client applications is dealing with overly chatty networks. We need to be mindful of how much we talk to services, especially when target audiences may have low connection speeds or limited data plans.

To deal with this problem at MLS, we actually bring in <a rel="noreferrer noopener" href="http://dev.apollodata.com/core/" target="_blank">apollo-client</a> directly. I’ve included it in this post because we’re going to create our own network interface to give to react-apollo — one that supports batching requests!

1import React from 'react';2import { ApolloClient, ApolloProvider } from 'react-apollo';3import { createBatchingNetworkInterface } from 'apollo-client';45import App from '...';67const client = new ApolloClient({8  networkInterface: createBatchingNetworkInterface({9    uri: '...', // have to provide uri to interface!10    batchInterval: 100, // time in ms to throttle queries11  }),12});1314export default () =>15  <ApolloProvider client={client}>16    <App />17  </ApolloProvider>;

4. Fallback Data Fetching

Sometimes you may want to show a fallback to users when you get an empty response from a particular query. At MLS we faced an issue where we wanted to display a match timeline if there were no highlights available. To solve this we either have to 1) fetch the data for both upfront every time (not ideal), or 2) see if we got a result for the highlights, and if not, request the timeline instead.

1import React from 'react';2import { compose, graphql } from 'react-apollo';3import { gql } from 'graphql-tools';45import MatchSummary from '...';6import MatchHighlights from '...';7import MatchTimeline from '...';89const MATCH_SUMMARY_QUERY = gql`10  query MatchSummary($date: Int) {11    match(date: $date) {12      id13      minute14      period15      home {16        score17      }18      away { 19        score20      }21    }22  }23`;2425const MATCH_HIGHLIGHTS_QUERY = gql`26  query MatchHighlights($date: Int) {27    match(date: $date) {28      id29      highlights {30        ...31      }32    }33  }34`;3536const MATCH_TIMELINE_QUERY = gql`37  query MatchTimeline($date: Int) {38    match(date: $date) {39      minute40      goals {41        ...42      }43      bookings {44        ...45      }46    }47  }48`;4950// timeline component to render if no highlights51const Timeline = ({ data: { loading, errors, match } }) =>52  <div className="timeline">53    {loading && <div className="loader" />}54    {errors && <div className="errors">...</div>}5556    {!loading && <MatchTimeline match={match} />}57  </div>;5859// connect to graphql, will only be called if this component is rendered60const TimelineWithData = graphql(MATCH_TIMELINE_QUERY)(Timeline);6162const Match = ({ data: { loading, errors, match } }) =>63  <div className="match">64    {loading && <div className="loader" />}65    {errors && <div className="errors">...</div>}6667    {!loading && <MatchSummary match={match} />}6869    {/* if we have highlights, render them, never render timeline */}70    {!loading && match.highlights && <MatchHiglights match={match} />}71     72    {73      /*74       * if we don't have highlights75       * render timeline instead and fetch timeline data76       */77    }78    {!loading && !match.highlights && <TimelineWithData />}79  </div>;8081export default compose(82  graphql(MATCH_SUMMARY_QUERY),83  graphql(MATCH_HIGHLIGHTS_QUERY),84)(Match);

5. Polling

At MLS we decided to move away from real-time push in favor of polling for a number of reasons, but a big driving factor was moving to GraphQL. Once we saw how easy it was to fine-tune data fetching in react-apollo via the pollInterval option on the config object, we knew we would easily be able to achieve that real-time feel that is necessary for our industry.

1import React from 'react';2import { graphql } from 'react-apollo';3import { gql } from 'graphql-tools';45import MatchSummary from '...';67const MATCH_SUMMARY_QUERY = gql`8  query MatchSummary($date: Int) {9    match(date: $date) {10      id11      minute12      period13      home {14        score15      }16      away { 17        score18      }19      highlights {...}20      goals {...}21      bookings {...}22    }23  }24`;2526const Match = ({ data: { loading, errors, match } }) =>27  <div className="match">28    {loading && <div className="loader" />}29    {errors && <div className="errors">...</div>}3031    {!loading && <MatchSummary match={match} />}32  </div>;3334export default graphql(MATCH_SUMMARY_QUERY, {35  // options passes props if given a function36  options: props => ({ pollInterval: props.pollInterval }),37})(Match);

If you’re curious about how we are implementing GraphQL on the backend, check out my other post on the MLS engineering blog. If you want to know more about the benefits we saw from moving to GraphQL, take a look at my co-worker Peggy Rayzis’s post on the subject!

If you have any questions about our experience, please feel free to leave a comment or reach out to me on the Twitter!

Written by

Kurt Kemple

Kurt Kemple

Read more by Kurt Kemple