GraphQL

Is Redux Still Necessary with Apollo GraphQL?

Apollo GraphQL Replaced Redux with apollo-link-state

The question of whether GraphQL supplants the need for Redux has lingered for quite a while now, likely because this is a fairly sophisticated problem.

Also perhaps because it herks back to the nostalgic era of building "old fashioned" REST APIs with Redux stores—no GraphQL, no Apollo or Relay.

A lot has been speculated about how GraphQL purpotedly replaces Redux. Obviously, GraphQL is server-side and Redux client-side.

So, is Redux necessary with Apollo GraphQL today?

NO. APOLLO GRAPHQL REPLACES THE NEED FOR REDUX ENTIRELY. Redux is a great library and the patterns (and anti-patterns) it introduced will stick around for a long time to come but libraries such as Relay and Apollo Client want to own the state they manage so that they can remove this responsibility of normalizing and denormalizing the data. These enables them to become the single source of truth for all application data - as opposed to trying to sync remote and local data between two stores. Let's explore what this means and how you can replace for Redux in Apollo Client with Apollo Client Link in your application.

Both Relay and Apollo also insist on managing the cache extensively and while still providing low-level access to the user.

Managing the Redux state yourself is not nearly as attractive.

Is Redux Necessary with Apollo GraphQL cover image

Using GraphQL in a Redux App

Migrating your REST API to GraphQL comes with a ton of outright benefits. If you can use GraphQL instead of REST, you probably should.

One of these benefits is getting rid of huge client-side state management, which significantly simplifies client side code to just rendereding UI components.

So, How Did Apollo GraphQL REPLACE Redux?

Application data has to be stored in a way that is accessible to all any component. This is a problem that quickly grows out of hand as the app expands.

By most estimates, remote data accounts for approx. 80% of application data while the other 20% is local data such as device API results and global flags.

Integrating GraphQL and Redux

Although Apollo is perfect for managing remote data, it used to be that the 20% was managed in a separate Redux store-hence the need for integrating GraphQL and Redux.

Redux is no longer necessary in Apollo GraphQL.

With Apollo Client 2.0 migrated away from Redux, keeping remote and local data synched between two stores become a nightmare for developers.

From here on out, keeping everything in Apollo to maintain a single souce of truth quickly became a priority.

This is where apollo-link-state comes in.

State Management with Apollo Client and apollo-link-state

One benefit of using Apollo Link State over Redux is that all the client side data is normalized out of the box. Which means you can dropping the Redux dependency for Apollo Link State is trivial...

Now Apollo Client is excellent at managing local application data without defaulting to separate Redux stores.

There are two goals to local state management with Apollo:

  1. We want to the ability to access local data from multiple components in our app: Ideally, things such as device API results and flags, and;
  2. We want Apollo cache to be the single source of truth for our application data...

...and obviously, we want to do this without a separate Redux store.

State Management in Apollo Client uses Apollo Link. This way, hooks can be inserted at any point of the GraphQL request cycle.

To request data from a GraphQL server, Apollo Client uses HttpLink, but to request data from the cache apollo-link-state is used.

apollo-link-state allows data to be stored inside the Apollo cache alongside remote data.

Application data can then be retrieved with normal GraphQL queries-in the same query as server data!

Implementing State Management with apollo-link-state

The following code initializes Apollo Client with apollo-link-state:

        
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloLink } from 'apollo-link';
import { withClientState } from 'apollo-link-state';
import { HttpLink } from 'apollo-link-http';

import { defaults, resolvers } from './resolvers/todos';

const cache = new InMemoryCache();

const stateLink = withClientState({ resolvers, cache, defaults });

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

withClientState is used to create a client state link, and then three options are passed in:

  1. resolvers
  2. defaults
  3. the Apollo cache

None of these three options are mandatory. If you do not specify anything, you will still be able to use the @client directive to query the cache.

Your state link should come before your HttpLinkso that local queries and mutations can be intercepted before they are sent.

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.

Your state link should also go apollo-persisted-queries if you are using persisted queries.

The defaults object contains the initial data you want to write to the Apollo cache when the client is initialized.

Although not required, the defaults option is passed in here to warm the cache so that any components querying the initial data do not return errors.

The defaults object indicates how you intend to query the data in your application.

        
export const defaults = {
  visibilityFilter: 'SHOW_ALL',
  todos: [],
};
        
      

The resolvers will be used to update and access the cache.

When managing the cache, the resolvers will become the single source of truth we are after for all local and remote data in your application.

The signature of the resolvers is as follows:

fieldName: (obj, args, context, info) => result;

The two most important things to note here are that:

  1. The query and mutations variables are passed in the second argument, and;
  2. The cache is automatically added to the context for you.

Updating Local Data with application-link-state

There are two methods of performing mutations after creating the initial cache in apollo-link-data:

  1. Directly writing to the cache by calling cache.writeData within a Query component: Direct writes are great for one-off mutations that do not depend on data currently in the cache, such as writing a single value.
  2. Creating a Mutation component with a GraphQL mutation that calls a client-side resolver. This is recommended if your mutation depends on existing values in the cache such as adding an item to a list or toggling a boolean - resolvers offer a bit more structure like in Redux.

The determining factor here is if your mutation depends on the data already in the cache, or it it is a one-off mutation.

The following code calls cache.writeData to update the cache, but uses cache.readQuery to read from the cache before performing a write.

        
export const defaults = { ... }

export const resolvers = {
  Mutation: {
    visibilityFilter: (_, { filter }, { cache }) => {
      cache.writeData({ data: { visibilityFilter: filter } });
      return null;
    },
    addTodo: (_, { text }, { cache }) => {
      const query = gql`
        query GetTodos {
          todos @client {
            id
            text
            completed
          }
        }
      `;
      const previous = cache.readQuery({ query });
      const newTodo = {
        id: nextTodoId++,
        text,
        completed: false,
        __typename: 'TodoItem',
      };
      const data = {
        todos: previous.todos.concat([newTodo]),
      };
      cache.writeData({ data });
      return newTodo;
    },
  }
}
        
      

The idea here is to write data to the root of the cache by cache.writeDatacalling and passing in our data.

cache.readQuery is called to read the cache before performing a write in addTodo and since we are using InMemoryCache, the key is __typename:id.

Another benefit of apollo-link-state is that it supports, async resolver functions. These are useful of side effects such as device APIs.

Instead of calling REST endpoints in your resolvers, you can use a library such as apollo-link-rest which has its own @rest directive.

The @client directive is used to specify client-only fields. When a function is triggered in the UI, Apollo needs to know whether to update the data on the client or the server.

The following code fragment performs a mutation on local data with the @client directive

        
const SET_VISIBILITY = gql`
  mutation SetFilter($filter: String!) {
    visibilityFilter(filter: $filter) @client
  }
`;

const setVisibilityFilter = graphql(SET_VISIBILITY, {
  props: ({ mutate, ownProps }) => ({
    onClick: () => mutate({ variables: { filter: ownProps.filter } }),
  }),
});
        
      

The queries will also look like the mutations. Apollo Client will track loading and errors automatically for asynchronious actions in queries.

And, you will be able to request multiple data sources in a single query as follows:

        
const GET_USERS_ACTIVE_TODOS = gql`
  {
    visibilityFilter @client
    user(id: 1) {
      name
      address
    }
  }
`;

const withActiveState = graphql(GET_USERS_ACTIVE_TODOS, {
  props: ({ ownProps, data }) => ({
    active: ownProps.filter === data.visibilityFilter,
    data,
  }),
});
        
      

You can read more on this in the apollo-link-state docs.