w3resource

Update local data with resolvers


In the last tutorial, we focused on querying local data from the Apollo cache. You may have wondered if there is a way of updating cached information in Apollo. Yes, Apollo Client also lets you update local data in the cache with either direct cache writes or client resolvers. Direct cache writes are typically used to write simple booleans or strings to the cache whereas client resolvers are for more complicated writes such as adding or removing data from a list.

Direct cache writes

Direct cache writes are convenient when you want to write a simple field, like a boolean or a string, to the Apollo cache. We perform a direct write by calling client.writeData() and passing in an object with a data property that corresponds to the data we want to write to the cache. We've already seen an example of a direct write, when we called client.writeData in the onCompleted handler for the login useMutation based component. Let's look at a similar example, where we copy the code below to create a logout button:

//src/containers/logout-button.jsx
import React from "react";
import styled from "react-emotion";
import { useApolloClient } from "@apollo/react-hooks";
import { menuItemClassName } from "../components/menu-item";
import { ReactComponent as ExitIcon } from "../assets/icons/exit.svg";

export default function LogoutButton() {
  const client = useApolloClient();
  return (
    <StyledButton
      onClick={() => {
        client.writeData({ data: { isLoggedIn: false } });        localStorage.clear();
      }}
   >
      <ExitIcon />
      Logout
    </StyledButton>
  );
}
const StyledButton = styled("button")(menuItemClassName, {
  background: "none",
  border: "none",
  padding: 0
});

When we click the button, we perform a direct cache write by calling client.writeData and passing in a data object that sets the isLoggedIn boolean to false.

We can also perform direct writes within the update function of the useMutation hook. The update function allows us to manually update the cache after a mutation occurs without refetching data. Let's look at an example in src/containers/book-trips.tsx:

//src/containers/book-trips.jsx
import React from 'react'; // preserve-line
import { useMutation } from '@apollo/react-hooks'; // preserve-line
import gql from 'graphql-tag';
import Button from '../components/button'; // preserve-line
import { GET_LAUNCH } from './cart-item'; // preserve-line
import * as GetCartItemsTypes from '../pages/__generated__/GetCartItems';
import * as BookTripsTypes from './__generated__/BookTrips';
export const BOOK_TRIPS = gql`
  mutation BookTrips($launchIds: [ID]!) {
    bookTrips(launchIds: $launchIds) {
      success
      message
      launches {
        id
        isBooked
      }
    }
  }
`;
interface BookTripsProps extends GetCartItemsTypes.GetCartItems {}
const BookTrips: React.FC<BookTripsProps> = ({ cartItems }) => {
  const [
    bookTrips, { data }
  ] = useMutation<
    BookTripsTypes.BookTrips, 
    BookTripsTypes.BookTripsVariables
  > (
    BOOK_TRIPS,
    {
      variables: { launchIds: cartItems },
      refetchQueries: cartItems.map(launchId => ({
        query: GET_LAUNCH,
        variables: { launchId },
      })),
      update(cache) {        cache.writeData({ data: { cartItems: [] } });      }    }
  );

  return data && data.bookTrips && !data.bookTrips.success
    ? <p data-testid="message">{data.bookTrips.message}</p>
    : (
      <Button 
        onClick={() => bookTrips()} 
        data-testid="book-button">
        Book All
      </Button>
    );
}

export default BookTrips;

In this example, we're directly calling cache.writeData to reset the state of the cartItems after the BookTrips mutation is sent to the server. This direct write is performed inside of the update function, which is passed our Apollo Client instance.

Local resolvers

We are getting there! What if we wanted to perform a more complicated local data update such as adding or removing items from a list? For this situation, we'll use a local resolver. Local resolvers have the same function signature as remote resolvers ((parent, args, context, info) => data). The only difference is that the Apollo cache is already added to the context for you. Inside your resolver, you'll use the cache to read and write data.

Let's write the local resolver for the addOrRemoveFromCart mutation. You should place this resolver underneath the Launch resolver we wrote earlier.

//src/resolvers.jsx
export const resolvers = {
  Mutation: {
    addOrRemoveFromCart: (_, { id }, { cache }) => {
      const queryResult = cache.readQuery({
        query: GET_CART_ITEMS
      });
      if (queryResult) {
        const { cartItems } = queryResult;
        const data = {
          cartItems: cartItems.includes(id)
            ? cartItems.filter(i => i !== id)
            : [...cartItems, id]
        };
        cache.writeQuery({ query: GET_CART_ITEMS, data });
        return data.cartItems;
      }
      return [];
    }
  }
};

In this resolver, we destructure the Apollo cache from the context in order to read the query that fetches cart items. Once we have our cart data, we either remove or add the cart item's id passed into the mutation to the list. Finally, we return the updated list from the mutation.

Let's see how we call the addOrRemoveFromCart mutation in a component:

//src/containers/action-button.jsx
import gql from "graphql-tag";
const TOGGLE_CART = gql`
  mutation addOrRemoveFromCart($launchId: ID!) {
    addOrRemoveFromCart(id: $launchId) @client
  }
`;

Just like before, the only thing we need to add to our mutation is a @client directive to tell Apollo to resolve this mutation from the cache instead of a remote server.

Now that our local mutation is complete, let's build out the rest of the ActionButton component so we can finish building the cart:

//src/containers/action-button.jsx
import React from "react";
import { useMutation } from "@apollo/react-hooks";
import gql from "graphql-tag";

import { GET_LAUNCH_DETAILS } from "../pages/launch";
import Button from "../components/button";

export const TOGGLE_CART = gql`
  mutation addOrRemoveFromCart($launchId: ID!) {
    addOrRemoveFromCart(id: $launchId) @client
  }
`;

export const CANCEL_TRIP = gql`
  mutation cancel($launchId: ID!) {
    cancelTrip(launchId: $launchId) {
      success
      message
      launches {
        id
        isBooked
      }
    }
  }
`;
const ActionButton = ({ isBooked, id, isInCart }) => {
  const [mutate, { loading, error }] = useMutation(
    isBooked ? CANCEL_TRIP : TOGGLE_CART,
    {
      variables: { launchId: id },
      refetchQueries: [
        {
          query: GET_LAUNCH_DETAILS,
          variables: { launchId: id }
        }
      ]
    }
  );

  if (loading) return <p>Loading...</p>;
  if (error) return <p>An error occurred</p>;

  return (
    <div>
      <Button onClick={() => mutate()} data-testid={"action-button"}>
        {isBooked
          ? "Cancel This Trip"
          : isInCart
          ? "Remove from Cart"
          : "Add to Cart"}
      </Button>
    </div>
  );
};

export default ActionButton;

In this example, we're using the isBooked prop passed into the component to determine which mutation we should fire. Just like remote mutations, we can pass in our local mutations to the same useMutation hook.

Hurray! We officially made it to the end of the Apollo platform tutorial. You can checkout other series of Apollo focused on the use of Apollo with other front end technologies.

Previous: Authenticate users
Next: local state management