Manage your application's state with Apollo!
Clone or download

README.md

apollo-link-state

Manage your local data with Apollo Client!

Docs | Announcement Post | Tutorial Video by Sara Vieira

Managing remote data from an external API is simple with Apollo Client, but where do we put all of our data that doesn't fit in that category? Nearly all apps need some way to centralize client-side data from user interactions and device APIs.

In the past, Apollo users stored their application's local data in a separate Redux or MobX store. With apollo-link-state, you no longer have to maintain a second store for local state. You can instead use the Apollo Client cache as your single source of truth that holds all of your local data alongside your remote data. To access or update your local state, you use GraphQL queries and mutations just like you would for data from a server.

When you use Apollo Client to manage your local state, you get all of the same benefits you know and love like caching and offline persistence without having to set these features up yourself. 🎉 On top of that, you also benefit from the Apollo DevTools for debugging and visibility into your store.

Quick start

To get started, install apollo-link-state from npm:

npm install apollo-link-state --save

The rest of the instructions assume that you have already set up Apollo Client in your application. After you install the package, you can create your state link by calling withClientState and passing in a resolver map. A resolver map describes how to retrieve and update your local data.

Let's look at an example where we're using a GraphQL mutation to update whether our network is connected with a boolean flag:

import { withClientState } from 'apollo-link-state';

// This is the same cache you pass into new ApolloClient
const cache = new InMemoryCache(...);

const stateLink = withClientState({
  cache,
  resolvers: {
    Mutation: {
      updateNetworkStatus: (_, { isConnected }, { cache }) => {
        const data = {
          networkStatus: {
            __typename: 'NetworkStatus',
            isConnected
          },
        };
        cache.writeData({ data });
        return null
      },
    },
  }
});

To hook up your state link to Apollo Client, add it to the other links in your Apollo Link chain. Your state link should be near the end of the chain, so that other links like apollo-link-error can also deal with local state requests. However, it should go before HttpLink so local queries and mutations are intercepted before they hit the network. It should also go before apollo-link-persisted-queries if you are using persisted queries. Then, pass your link chain to the Apollo Client constructor.

const client = new ApolloClient({
  cache,
  link: ApolloLink.from([
    stateLink,
    new HttpLink()
  ]),
});

How do we differentiate a request for local data from a request that hits our server? In our query or mutation, we specify which fields are client-only with a @client directive. This tells our network stack to retrieve or update the data in the cache with our resolver map that we passed into our state link.

const UPDATE_NETWORK_STATUS = gql`
  mutation updateNetworkStatus($isConnected: Boolean) {
    updateNetworkStatus(isConnected: $isConnected) @client
  }
`;

To fire off the mutation from your component, bind your mutation to your component via your favorite Apollo view layer integration just like you normally would. Here's what this would look like for React:

const WrappedComponent = graphql(UPDATE_NETWORK_STATUS, {
  props: ({ mutate }) => ({
    updateNetworkStatus: isConnected => mutate({ variables: { isConnected } }),
  }),
})(NetworkStatus);

What if we want to access our network status data from another component? Since we don't know whether our UPDATE_NETWORK_STATUS mutation will fire before we try to access the data, we should guard against undefined values by providing a default state as part of the state link initialization:

const stateLink = withClientState({
  cache,
  resolvers: {
    Mutation: {
      /* same as above */
    },
  },
  defaults: {
    networkStatus: {
      __typename: 'NetworkStatus',
      isConnected: true,
    }
  },
});

This is the same as calling writeData yourself with an initial value:

// Same as passing defaults above
cache.writeData({
  networkStatus: {
    __typename: 'NetworkStatus',
    isConnected: true,
  }
});

How do we query the networkStatus from our component? Similar to mutations, just use a query and the @client directive! With Apollo Link, we can combine data sources, including your remote data, in one query.

In this example, the articles field will either hit the cache or fetch from our GraphQL endpoint, depending on our fetch policy. Since networkStatus is marked with @client, we know that this is local data, so it will resolve from the cache.

const GET_ARTICLES = gql`
  query {
    networkStatus @client {
      isConnected
    }
    articles {
      id
      title
    }
  }
`;

To retrieve the data in your component, bind your query to your component via your favorite Apollo view layer integration just like you normally would. In this case, we'll use React as an example. React Apollo will attach both your remote and local data to props.data while tracking both loading and error states. Once the query returns a result, your component will update reactively. Updates to Apollo Client state via apollo-link-state will also automatically update any components using that data in a query.

const WrappedComponent = graphql(GET_ARTICLES, {
  props: ({ data: { networkStatus, articles, loading, error } }) => {
    if (loading) {
      return { loading };
    }

    if (error) {
      return { error };
    }

    return {
      loading,
      networkStatus,
      articles,
    };
  },
})(Articles);

For more detailed examples, plus in-depth explanations of resolvers, defaults, and more, please check out our official docs page.

With Apollo Boost

If you are using apollo-boost, it already includes apollo-link-state underneath the hood for you. Instead of passing the link property when instantiating Apollo Client, you pass in clientState.

import ApolloClient from 'apollo-boost';

const client = new ApolloClient({
  clientState: {
    defaults: {
      isConnected: true
    },
    resolvers: {
      Mutation: {
        updateNetworkStatus: (_, { isConnected }, { cache }) => {
          cache.writeData({ data: { isConnected }});
          return null;
        }
      }
    }
  }
});

Local Development

If you're setting up for local development, and you want to integrate a local branch of apollo-link-state into another application, remember that this project is a Lerna monorepo: ./packages/apollo-link-state

To link this in, do:

cd packages/apollo-link-state && yarn link

And in your development application do:

yarn link apollo-link-state

Finally, each time you make a change in apollo-link-state, you need to run:

yarn build && yarn bundle

Now you should be good to go!