Iām a firm believer that the best code is no code. More code often leads to more bugs and more time spent maintaining it. At Major League Soccer, weāre a very small team, so we take this principle to heart. We try to optimize where we can, either through maximizing code reuse or lightening our maintenance burden.
In this article, youāll learn how we offloaded data fetching management to Apollo, which allowed us to delete nearly 5,000 lines of code. Not only is our application a lot slimmer since switching to Apollo, itās also more declarative since our components only request the data that they need.
What do I mean by declarative and why is that a good thing? Declarative programming focuses on the end goal, while imperative programming focuses on the steps it took to get there. React itself is declarative.
Fetching data with Redux
Letās look at a simple Article component:
import React from 'react';
import { View, Text } from 'react-native';
export default ({ title, body }) => (
<View>
<Text>{title}</Text>
<Text>{body}</Text>
</View>
);
Now, letās say we want to render <Article/>
in a connected <MatchDetail/>
view, which takes a match ID as a prop. If we were accomplishing this without a GraphQL client, our process to retrieve the data necessary to render <Article/>
might look something like this:
- When
<MatchDetail/>
mounts, invoke action creator to fetch match by ID. Action creator dispatches action to tell Redux weāre fetching. - We hit an endpoint and receive data back. We normalize the data into the structure we need.
- Once the data is in the structure we need, we dispatch an action to tell Redux weāre done fetching.
- Redux processes the action in our reducer and updates our state tree
<MatchDetail/>
receives all of the match data via props and filters it down to render the article.
Thatās a lot of steps to get the data for <Article/>
! Without a GraphQL client, our code is much more imperative because we have to focus on how weāre retrieving our data. What if we donāt want to pull down all of the match data just to render <Article/>
? You could build another endpoint and create a separate set of action creators for hitting it, but that can quickly become unmaintainable.
Letās contrast with how you could approach this with GraphQL:
<MatchDetail/>
is connected with a higher order component that fetches the following query:
query Article($id: Float!) { match(id: $id) { article { title body } } }
ā¦and thatās it! Once the client receives the data, it will map it to props that can be passed down to <Article/>
. Much more declarative, because weāre only focusing on what data we need to render the component.
This is the beauty of delegating data fetching to a GraphQL client, whether itās Relay or Apollo. When you start āthinking in GraphQL,ā you become more concerned with what props your component needs to render and less concerned with how youāre going to get them.
At some point, you will need to take care of the āhow,ā but this concern is now server-side and the complexity is drastically reduced. If youāre new to GraphQL server architecture, make sure you check out <a href="http://dev.apollodata.com/tools/graphql-tools/index.html" target="_blank" rel="noreferrer noopener">graphql-tools</a>
, Apolloās library that helps you structure your schema in a modular way. For brevityās sake, we will be focusing on the front-end today.
Although this post will teach you how to reduce your Redux code, you wonāt be getting rid of it entirely! Apollo uses Redux under the hood, so you still get the benefit of immutability and all of your favorite Redux Dev Tools features like time-travel debugging. During setup, you can hook Apollo into your existing Redux store to maintain one source of truth. Once your store is configured, you pass it into a <ApolloProvider/>
component that wraps your application. Sound familiar? This component replaces your existing <Provider />
from Redux, except you also need to pass down your ApolloClient
instance through the client prop.
Before we start slicing up our Redux code, I want to call out one of the best features of GraphQL: incremental adoption. You donāt have to commit to refactoring your entire application at once. Since Apollo integrates into your existing Redux store, you can switch over your reducers incrementally. The same thing applies to the backend ā if youāre working on a large scale application, you can use GraphQL side by side with your existing REST endpoints until youāre ready to convert them. Fair warning: once you try it, you will fall in love with it and want to refactor your entire application. š
Our requirements
Prior to switching from Redux, we thought carefully about whether Apollo would meet our needs. Hereās what we looked at when making our decision:
- Aggregating data from multiple sources: A match is comprised of data from 4 different sources: content from our REST API, stats from our MySQL database, media from our video API, and social data from our Redis store. Originally, we were using a server plugin to gather all of the data into one match object to send to the client. It almost functioned just like a GraphQL layer! As soon as we realized this, it became apparent that our application would be a perfect candidate for GraphQL.
- Near realtime updates: During a live match, we typically receive updates every minute. Before Apollo, we were handling live updates with sockets and dispatching them to our match reducer. This wasnāt a terrible solution, but it wasnāt the most elegant as we were sending down an entire match object to avoid complicated sequencing. With Apollo, we can easily customize a polling interval per component depending on the gameās status.
- Simple pagination: Since we were building out a schedule page with an infinitely scrolling list of matches, we needed a way to handle pagination that wasnāt headache inducing. Sure, we could have built a custom reducer. But why write it ourselves when Apolloās
fetchMore
function does all the heavy lifting for us? šŖ
Not only did Apollo satisfy our current requirements, it also covered some of our future needs, especially since enhanced personalization is on our roadmap. While our server is currently āread only,ā we may need to introduce mutations in the future to save a userās favorite team. If we decide to add realtime commenting or fan interaction that canāt be solved with polling, Apollo supports subscriptions.
From Redux to Apollo š
The moment youāve been waiting for! Originally, when I thought of writing this post, I was going to show before/after code samples, but I think itās difficult to directly compare the two approaches, especially if youāre new to Apollo. Instead, Iām going to quantify what we deleted entirely and walk you through familiar concepts in Redux that you can apply to building your container components in Apollo.
What we deleted
- Matches reducer (~300 lines of code)
- Data fetching action creators & epics (~800 LOC)
- Action creators & business logic for batching & receiving socket updates for live matches (~750 LOC)
- Local storage action creators & epics (~1000 LOC). This is a bit unfair to count in the total because offline support for our project is postponed, but itās achievable if we want to add it back in by customizing the Apollo
fetchPolicy
& exposing the reducer toredux-persist
. - Redux container components that separated our Redux logic from our presentational components (~1000 LOC)
- Tests associated with all of the above (~1000 LOC)
connect() ā graphql()
If you know how to use connect
, then Apolloās graphql
higher order component will seem very familiar! Just like connect
returns a function that takes a component and connects it to your Redux store, graphql
returns a function that takes a component and āconnectsā it to Apollo Client. Letās see it in action!
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import { MatchSummary, NoDataSummary } from '@mls-digital/react-components';
import MatchSummaryQuery from './match-summary.graphql';
// here we're using the graphql HOC as a decorator, but you can use it as a function too!
@graphql(MatchSummaryQuery, {
options: ({ id, season, shouldPoll }) => {
return {
variables: {
id,
season,
},
pollInterval: shouldPoll ? 1000 * 60 : undefined,
};
};
})
class MatchSummaryContainer extends Component {
render() {
const { data: { loading, match } } = this.props;
if (loading && !match) {
return <NoDataSummary />;
}
return <MatchSummary {...match} />;
}
}
export default MatchSummaryContainer;
The first argument supplied to graphql
is our MatchSummaryQuery
. This is the data we want to receive back from our server. Weāre using a Webpack loader to parse our query into the GraphQL AST, but if youāre not using Webpack, you will need to wrap your query in a template string and pass it into the <a rel="noreferrer noopener" href="http://dev.apollodata.com/react/api.html#gql" target="_blank">gql</a>
function exported from Apollo. Hereās an example of the query to fetch the data needed for this component:
query MatchSummary($id: String!, $season: String) {
match(id: $id) {
stats {
scores {
home {
score
isWinner: is_winner
}
away {
score
isWinner: is_winner
}
}
}
home {
id: opta_id
record(season: $season)
}
away {
id: opta_id
record(season: $season)
}
}
}
Great, we have our query! In order for this query to execute, weāre going to need to supply it with two variables, $id
and $season
. Where are we going to get those variables from? š¤ Thatās where the second argument to graphql
comes in, our config object.
The config object has several properties that you can specify to customize the behavior of your HOC. One of the most important is the options
property, which takes a function that receives your container componentās props. This function returns an object with properties like variables
, which supplies your variables to your query, and pollInterval
, which allows you to customize your componentās polling behavior. Notice how weāre using our container componentās props to supply id
and season
to our MatchSummaryQuery
. If this function gets too long to write it in the decorator, weāll break it out into its own function called mapPropsToOptions
.
mapStateToProps() ā mapResultsToProps()
In your Redux containers, you probably wrote a function called mapStateToProps
that you passed to connect
in order to map data from your state tree into props to pass down to the component. Apollo allows you to define a similar function. Remember the config object from earlier that we passed into our graphql
function? The config object also has another property, props
, that receives a function that takes props and maps them before passing them down to your container. You can define it inline if you want, but we like to call ours mapResultsToProps
.
Why would you want to map your props? Your results from your GraphQL query will be attached to the data
prop. Sometimes, you might need to flatten this data before passing it to your component. Hereās an example:
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import { MatchSummary, NoDataSummary } from '@mls-digital/react-components';
import MatchSummaryQuery from './match-summary.graphql';
const mapResultsToProps = ({ data }) => {
if (!data.match)
return {
loading: data.loading,
};
const { stats, home, away } = data.match;
return {
loading: data.loading,
home: {
...home,
results: stats.scores.home,
},
away: {
...away,
results: stats.scores.away,
},
};
};
const mapPropsToOptions = ({ id, season, shouldPoll }) => {
return {
variables: {
id,
season,
},
pollInterval: shouldPoll ? 1000 * 60 : undefined,
};
};
@graphql(MatchSummaryQuery, {
props: mapResultsToProps,
options: mapPropsToOptions,
})
class MatchSummaryContainer extends Component {
render() {
const { loading, ...matchSummaryProps } = this.props;
if (loading && !matchSummaryProps.home) {
return <NoDataSummary />;
}
return <MatchSummary {...matchSummaryProps} />;
}
}
export default MatchSummaryContainer;
Not only does the data object contain your query results, it also contains properties like data.loading
to let you know if your query still hasnāt returned a response. This can be useful if you want to display another component to your user, like weāre doing with <NoDataSummary />
.
compose()
Compose isnāt a function thatās unique to Redux, but I do want to point out that Apollo includes it for your convenience. This is super useful if you want to compose several graphql
functions to be used by one container. You can even use compose
with Redux connect
and graphql
together! Hereās how we use compose
to display different match tile states:
import React, { Component } from 'react';
import { compose, graphql } from 'react-apollo';
import { NoDataExtension } from '@mls-digital/react-components';
import PostGameExtension from './post-game';
import PreGameExtension from './pre-game';
import PostGameQuery from './post-game.graphql';
import PreGameQuery from './pre-game.graphql';
@compose(
graphql(PreGameQuery, {
skip: ({ gameStatus }) => gameStatus !== 'pre',
props: ({ data }) => ({
preGameLoading: data.loading,
preGameProps: data.match,
}),
}),
graphql(PostGameQuery, {
skip: ({ gameStatus }) => gameStatus !== 'post',
props: ({ data }) => ({
postGameLoading: data.loading,
postGameProps: data.match,
}),
}),
)
export default class MatchExtensionContainer extends Component {
render() {
const {
preGameLoading,
postGameLoading,
gameStatus,
preGameProps,
postGameProps,
...rest
} = this.props;
if (preGameLoading || postGameLoading)
return <NoDataExtension gameStatus={gameStatus} />;
return gameStatus === 'post'
? <PostGameExtension {...postGameProps} {...rest} />;
: <PreGameExtension {...preGameProps} {...rest} />;
}
}
compose
is great for when your container has multiple states. What if you need to execute a different query depending on its state? Thatās where skip
comes in, as you can see above on the config object. The skip
property takes a function that receives props and allows you to skip the query if it doesnāt meet the criteria you specify. compose
+ skip
= š
All of these examples demonstrate that if you know Redux, youāll pick up Apollo quickly! Its API draws upon many Redux concepts while reducing the code you need to write to achieve the same results.
I hope learning about Major League Soccerās experience switching to Apollo was helpful! As with any library decision, the best solution to manage your applicationās data fetching will depend on your projectās requirements. If you have any questions about our experience, please feel free to leave a comment or reach out to me on Twitter! š