Loading Patterns in Apollo Client
Blog

Loading Patterns in Apollo Client

Loading State

How you handle loading states plays a huge role in the retention of your user base, and there isn’t a direct solution for handling loading states in Apollo Client. In addition, loading state is more complex than it may first seem. Here are some of the sub-states you may need to deal with in loading:

Initial Loading

Active Re-fetching (example: re-fetch data)

Passive Re-fetching (example: polling data, setting variables)

Fetching More (example: infinite scroll, load more)

Each of these loading states need to be handled in a different way. This adds some complexity to your app and your user experience design.

Apollo Client doesn’t handle this for you, and for a good reason. It’s not up to them to make any assumptions about your use cases, especially when you consider the countless ways in which you might want to handle these states. However, in your everyday data loading, social-style application, the way you handle these states is fairly consistent. From the perspective of the designer, it’s smart to keep it that way, for the sake of adoption. A perfect example of this, is your typical social media app like Facebook and Twitter, all which consistently use the states listed above.

How Apollo Client handles states

Apollo Client handles states in two ways: loading and networkStatus. In the <ApolloClientResponse> loading and networkStatus are passed alongside your data. loading provides a boolean which leaves you with very little flexibility. networkStatus is a number between 1 and 8 which relates to each different state of the Apollo Client Query (including an error state). I want to make it very clear that networkStatus isn’t a loading state but instead the state of the GraphQL query at any given time. Although this provides rich information, it isn’t very usable straight out of the box when it comes to pragmatically handling your apps loading states. We’ll elaborate on this later, for now, let’s take a look at loading and networkStatus.

Loading

So, why is the lack of flexibility an issue? Let’s go over a basic user workflow:

workflow-one

For a very simple app this might be fine, because they might not use anything other than the initial load of the data. But in most cases features like fetchMore, refetch, and polling are commonly used. Here is an example of where loading falls through:

workflow-two

We are running the same workflow but we adding in a re-fetch query. When you re-fetch your GraphQL query the generic loading state is set to true. If you manage your loading states from a boolean you are limited to just one visual loading state to display for any of the loading sub-states. In this case, you’re removing the data that was already displayed (and cached) and showing the same loading screen as the initial load. Simply put, this is a poor user experience that will also occur for fetching more, polling data etc. So in most applications loading isn’t a viable option.

Network Status

networkStatus works in a different way. It provides a single number that represents 1 of 7 different states of your queries execution. This is a lot better and lot more flexible than the loading flag. Let’s look at how networkStatus works. networkStatus can be any number between 1 and 8 (5 isn’t an option as of v2):

1: loading

2: setVariables

3: fetchMore

4: refetch

6: poll

7: ready

8: error

If we ran through the same user workflow, you can see how we would be able to provide a better user experience.

workflow-three

We have a lot more information here and it certainly allows us to better handle the loading states we have mentioned above. However, it’s not very useable. Using it straight out the box will leave you with code like the following:

// imports removed for brevity

const App = ({
  data: {
    list,
    refetch,
    fetchMore
  },
  loading,
  networkStatus
}) => ({
  <View style={styles.container}>
    {networkStatus === 1 ?
      <LoadingSkeleton />
      :
      <ListView
        dataSource={list}
        refreshControl={
          <RefreshControl
            refreshing={networkStatus === 4}                                    
            onRefresh={refetch}
          />
        }
        renderFooter={() =>
          networkStatus === 3 && <FetchMoreSkeleton />
        }
        onEndReached={fetchMore}
      />
    }
  </View>
});

const query = graphql(gql`
  query {
    list
  }
`, {
  options: {
    fetchPolicy: 'cache-and-network',
    notifyOnNetworkStatusChange: true, // needed for networkStatus
  },
});

compose(query)(App);

This code is not readable nor easily maintainable, but it does provide all the information we need to please our users. How can we improve this?

An Alternative

We’ve talked about what Apollo Client provides and where its caveats lie. I want to provide an alternative.

You might use a higher order component (HOC) in the React components to provide a status for your component. This example will use helper functions from the recompose library

import { branch, renderComponent } from 'recompose';

// Loading States
const initialLoading = networkStatus => networkStatus === 1;
const activelyRefetching = networkStatus => networkStatus === 4;
const passivelyRefetching = networkStatus =>
  networkStatus === 2 || networkStatus === 6;
const fetchingMore = networkStatus => networkStatus === 3;

// Error States
export const error = networkStatus => networkStatus === 8;

// State HOCS
export const loadingState = Component => branch(
  props => initialLoading(props.networkStatus),
  renderComponent(Component),
  () => ({
    activelyRefetching: activelyRefetching(props.networkStatus),
    passivelyRefetching: passivelyRefetching(props.networkStatus),
    fetchingMore: fetchingMore(props.networkStatus)
  })
);

export const errorState = Component => branch(
  props => error(props.networkStatus),
  renderComponent(Component)
);

// list networkStatus definitions for maintainability below

In the above code we’re simply filtering each Apollo Client state into the 4 previously named loading sub-states, these states might be different for your application, but you can customize this code to meet your needs. But again, generally this will meet the needs of a standard app.

The loading HOC will handle the initial load with a component you pass in yourself and then deliver 3 readable props with the alternate loading sub-states.

Now let’s rebuild the above component with the new HOC:

import { compose } from 'recompose'
import { loadingState, errorState } from './hoc';
// other imports removed for brevity

const LoadingSkeleton = () => (/* your loading component */)

const Component = ({
  data: {
    list,
    refetch,
    fetchMore
  },
  activelyRefetching,
  passivelyRefetching,
  fetchingMore,
}) => ({
  <View style={styles.container}>
    <ListView
      dataSource={list}
      refreshControl={
        <RefreshControl
          refreshing={activelyRefetching}
          onRefresh={refetch}
        />
      }
      renderFooter={() => fetchingMore && <FetchMoreSkeleton />}
      onEndReached={fetchMore}
    />
  </View>
});

const query = graphql(/* same as above, removed for brevity */);

compose(
  query,
  loadingState(LoadingSkeleton),
  errorState(ErrorComponent),
)(Component);

note: passivelyRefetching isn’t being used in these examples, a good example of a passive re-fetch is the Apple Stocks app that polls data iteratively and uses the Apple menu bar loading indicator to display that.

You can see the improvement in readability and this will let you update your HOC once throughout your codebase if networkStatus is changed in the future.

Take-Away

Understanding the information Apollo Client gives you is the first step in managing your loading states — handling that data through a higher order component provides you with a maintainable and readable way to craft a rich loading experience.

We'll share what we've learned, get tips and info in your inbox occasionally