Apollo client - things I did not know

Published on

Apollo Client automatically adds the __typename

When you make a query via Apollo Client, it automatically adds the __typename field to every object in your queries and mutations.

So this is why if you look at the network response, you always see the type added:

{
  "__typename": "Todo",
  "id": "5",
  "text": "Buy grapes 🍇"
}

The Apollo cache is based on IDs

By ID, I mean the field that is defined as an ID in the graph ql schema. (It doesn't have to have a key of id).

So for this response:

{
  "__typename": "Todo",
  "id": "5",
  "text": "Buy grapes 🍇"
}

Apollo Client caches it with key Todo:5. If a cached object already exists with this key, Apollo Client overwrites any existing fields that are also included in the mutation response (other existing fields are preserved).

One thing to note: a newly cached object isn't automatically added to any list fields that should now include that object.

Using variables helps with caching

You could send either of these queries (and the variables in the top one), and they would return the same thing:

// recommended way - with vars
query GetDog($dogId: ID!) {
  dog(id: $dogId) {
    name
    breed
  }
}

// not recommended - hard coded
query GetDog {
  dog(id: "5") {
    name
    breed
  }
}

There are a few reasons why using variables is beneficial:

  • Easier to reuse the query with different variables
  • Better automatic cache logic
  • Better security. The value of a GraphQL argument might include sensitive information, such as an access token or a user's personal info. If this information is included in a query string, it's cached with the rest of that query string. Variable values are not included in query strings. You can also specify which variable values (if any) are included in metrics reporting to Apollo Studio.

Query global data and user-specific data separately

If you have two queries - one which is specific to a user (such as logged in profile info), and one that is global (such as all products for sale), split them into separate queries.

If you combine them into one, server side caching is much harder.

If you fetch these in separate queries, the server response for the global query can be cached for all users. If they're combined this is harder/impossible.

Set ApolloClient name/version

You can easily set up the name/version, which can be used by logging tools.

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache(),
  name: 'MarketingSite',
  version: '1.2'
});

How the cache works

Although the response data might be nested (like if you have an author, with all books under that), the Apollo cache is flat. It goes through a few steps:

To start with it parses the query response, and finds all of the distinct objects.

For the following response, it would find two distinct objects (a Person with ID cGVvcGxlOjE=, and a Planet with ID cGxhbmV0czox):

{
  "data": {
    "person": {
      "__typename": "Person",
      "id": "cGVvcGxlOjE=",
      "name": "Luke Skywalker",
      "homeworld": {
        "__typename": "Planet",
        "id": "cGxhbmV0czox",
        "name": "Tatooine"
      }
    }
  }
}

Then it will create a cache ID for each object. These are by default the type and ID joined together into a string with : separating them.

So the cache IDs from the above query response would be Person:cGVvcGxlOjE= and Planet:cGxhbmV0czox.

It is easy to change these by creating a new instance of InMemoryCache with the config in typePolicies.


import { InMemoryCache, ApolloClient } from '@apollo/client';
const cache = new InMemoryCache({
  typePolicies: {
    Product: {
      // In an inventory management system, products might be identified
      // by their UPC.
      keyFields: ["upc"],
    },
    Person: {
      // In a user account system, the combination of a person's name AND email
      // address might uniquely identify them.
      keyFields: ["name", "email"],
    },
    Book: {
      // If one of the keyFields is an object with fields of its own, you can
      // include those nested keyFields by using a nested array of strings:
      keyFields: ["title", "author", ["name"]],
    },
    AllProducts: {
      // Singleton types that have no identifying field can use an empty
      // array for their keyFields.
      keyFields: [],
    },
  },
});


const client = new ApolloClient({
  // ...other arguments...
  cache: new InMemoryCache(options)
});

Now it has the cache ID, it uses that to store the data for that object. Then it replaces all fields that point to that ID with a reference to the object in the cache.

So it goes from something like this:

{
  "__typename": "Person",
  "id": "cGVvcGxlOjE=",
  "name": "Luke Skywalker",
  "homeworld": {
    "__typename": "Planet",
    "id": "cGxhbmV0czox",
    "name": "Tatooine"
  }
}

and replaces it with this:

{
  "__typename": "Person",
  "id": "cGVvcGxlOjE=",
  "name": "Luke Skywalker",
  "homeworld": {
    "__ref": "Planet:cGxhbmV0czox" // << refers to the cache id for the Planet
  }
}

Now it is easy for Apollo to lookup any of the cached items. When a new incoming object comes in with an ID that is already cached, the fields are merged.

  • If the incoming object and the existing object share any fields, the incoming object overwrites the cached values for those fields.
  • Fields that appear in only the existing object or only the incoming object are preserved.
  • Normalization constructs a partial copy of your graph on your client, in a format that's optimized for reading and updating as your app's state changes.