Implementing a Shopping Cart with ApolloJS 3
Tags: apollojs, graphql, react, apollo
Published: 2021-06-18 14:37:26

ApolloJS is a GraphQL framework that lifts a lot of heavy work in both client and server. It also aims to provide a good solution for local state management in client, but one can quickly see it is still very young in this business: The docs give 3 different solutions for state management, but none of them is extensibly documented. Also, none of them allow for event dispatching nor state machines.

In the official ApolloJS docs, there is half an example of a shopping cart. As the lack of explanation left me puzzled, I tried several things, and I am going to explain here a solution that is both simple and idiomatic to Apollo.

## Ingredients

In this example we are assuming we have:
- A datasource with methods `getItem(itemId)` and `getAllItems()`
- A GraphQL proxy, implemented with apollo-server
- Ability to edit this proxy
- The next schema:

```graphql
type Item {
  id: String
  name: String
  price: Int
}

type Query {
  allItems: [Item!]!
  item(id: String!): Item
}
```

## The Cart in the Client

In order to implement the cart, we want to store in the client's state the minimum amount of data we can.

A possible implementation would be to have a fully-fledged store and replicate there the data of all the selected items in the cart, but we already have this data in the Apollo cache, and we want to take advantage of that.

The minimum data we need is **the list of selected IDs**, so that's what we will be storing.

But what happens if we haven't fetched the selected items yet? We will need a way to fetch their data, but we only have a way to get one or all the items. Even worse: In a real case, the `allItems` query will be paginated and we won't have any guarantee we have fetched the selected items.

## The Server

In order to fetch the missing data, we will need a query that fetches *only* the selected items.

Let's add the new query to the schema:

```graphql
type Query {
  allItems: [Item!]!
  item(id: String!): Item
  items(ids: [String!]!): [Item!]!
}
```

We also need to add the appropriate resolver:

```javascript
const resolvers = {
  items: (_, {ids}, {dataSources}) => (
    Promise.all(ids.map(
      id => dataSources.itemsAPI.getItem(id)
    ))
  ),
  ...
}
```

## The Client

In order to have local state in Apollo, we extend the schema with local fields as follows:

```javascript
const typeDefs = gql`
  extend type Query {
    cartItemsIds: [Int!]!
  }
`
```


Apollo gives us three ways of handling this local state, each one worse than the rest:

### Rolling our own solution
This means having our own local dataSource (localStorage, Redux store, etc).

In order to read the data, we can write a read resolver for our client queries that resolve against this local dataSource.

In order to modify the data, the documentation doesn't say anywhere that we can write resolvers for mutations, and tells us to directly call the dataSource, coupling it everywhere, and afterwards calling manually `cache.evict({id, fieldName})` in order to force the refresh of all the dependents of the modified entity.

### Using the cache
Just as in the previous, we write a read resolver, but we will use Apollo's cache itself as a dataSource, thus avoiding the call to `cache.evict`.

This means we will have to call `readQuery` **with a GraphQL query in order to resolve a GraphQL query**. It also means we will need to add types to the extended schema, and that we won't be able to store anything that is not a cacheable entity (has an ID) or is not directly related to one.

We want to store an array of IDs, which shouldn't need to have an ID on its own because it is not an instance of anything.

This solution would force us to implement this as a boolean `isInCart` client field in the `Item` itself, querying the cache and filtering all items that have `isInCart === true`. It is fine for the cart case, but not extensible to things that are not related to entities in the cache. We don't want to be forced to use different methods for different local data.

It will also force us to call directly `writeQuery` in order to modify the data. All in all, suboptimal at best.

### Reactive Variables
**The chosen solution.**
We create a global (ehem) reactive variable, then write a resolver that will retrieve its value, and we can also check and modify the variable in any component using the `useReactiveVar` hook.

This solution still forces us to read data using a different paradigm that the way we write it. However, we won't have to use `cache.evict` nor the suicide-inducer cache API.

## Client Implementation

We create the reactive variable and check it in the resolver for our local `cartItemsIds` query:
```javascript
const itemsInCart = makeVar([]) // We start with no item selected

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: { // boilerplate
        cartItemIds: {
          read() {
            return itemsInCart()
          }
        }
      }
    }
  }
})

const client = new ApolloClient({
  uri: 'https://...',
  typeDefs,
  cache,
})
```

Now we can make the following client query from any component:

```graphql
query ItemIdsInCart {
  cartItemsIds @client
}
```

And we can combine this query with the new server query in order to get all the data for each item:

```javascript
const GET_CART = gql`
  query GetCart($itemIds: [String!]!) {
    cartItemIds @client @export(as: "itemIds")
    items(ids: $itemIds) {
      id
      name
      price
    }
  }
`

const Cart = () => {
  const {loading, error, data} = useQuery(GET_CART)
  if (loading || error) return null
  return (
    <ul>
      {data.items.map(item => (
        <li key={item.id}>
          {`${item.name}...${item.price}$`
        </li>
      ))}
    </ul>
  )
}
```

## Even a better solution

If we look closely, we will see we could fetch the reactive variable from the component, and thus avoid the local query altogether. Let's see how:

First, we ignore Apollo docs and remove the pyramid of doom from the InMemoryCache:

```javascript
const itemsInCart = makeVar([])

const client = new ApolloClient({
  uri: 'https://...',
  cache: new InMemoryCache(),
  // no typeDefs either
})
```

Now, we can use the reactive variable directly in the component without any sense of guilt:

```javascript
const GET_CART = gql`
  query GetCart($itemIds: [String!]!) {
    items(ids: $itemIds) {
      id
      name
      price
    }
  }
`

const Cart = () => {
  const cartItemIds = useReactiveVar(itemsInCart)
  const {loading, error, data} = useQuery(GET_CART, {
    variables: {itemIds: cartItemIds},
  })
  if (loading || error) return null
  return (
    <ul>
      {data.items.map(item => (
        <li key={item.id}>
          {`${item.name}...${item.price}$`}
        </li>
      ))}
    </ul>
  )
}
```

## Modifying the Cart

So how do we modify the variable? We call it with the new value, and all dependents will magically update and all queries will be refetched.

We will add a removeFromCart function to the component to see how this works:

```javascript
const Cart = () => {
  const cartItemIds = useReactiveVar(itemsInCart)
  // + vvv
  const removeFromCart = useCallback(id => {
    const remainingItems = cartItemIds.filter(item => item !== id)
    // This will trigger the re-render due to useReactiveVar
    itemsInCart(remainingItems)
  }, [cartItemIds])
  // + ^^^
  const {loading, error, data} = useQuery(GET_CART, {
    variables: {itemIds: cartItemIds},
  })
  if (loading || error) return null
  return (
    <ul>
      {// Call removeFromCart on click
      data.items.map(item => (
        <li key={item.id} onClick={() => removeFromCart(item.id)}>
          {`${item.name}...${item.price}$`
        </li>
      ))}
    </ul>
  )
}
```

## Conclusions

You can find the full code here:
Server: [codesandbox.io/s/awesome-northcutt-iwgxh](codesandbox.io/s/awesome-northcutt-iwgxh)
Client: [codesandbox.io/s/elegant-mclean-ekekk](codesandbox.io/s/elegant-mclean-ekekk)

A special thanks to this article by Johnny Magrippis for the environment set-up:
[https://medium.com/javascript-in-plain-english/fullstack-javascript-graphql-in-5-with-code-sandbox-374cfec2dd0e](https://medium.com/javascript-in-plain-english/fullstack-javascript-graphql-in-5-with-code-sandbox-374cfec2dd0e)

**What is the utility of custom local-only fields, then?**

As far as I have seen, none. I haven't found any way to make local queries derive the output from several remote queries. As these dependencies are meant to be solved in the component, we may as well connect it to Redux for all the local state and make all the queries based on the values in the state. We will have full reactivity as well, and a coherent way to get and set all local state.

I don't have a lot of experience with Apollo and this conclusion should be taken cautiously. This article is only meant as a tutorial as well as a critique to Apollo's incomplete docs.

If this helped you in any way or you know more than I do, please let me know.