Client-side State with Apollo

Introduction to GraphQL

1. WTF Is GraphQL and why should I care
2. Schema Design
3. Resolvers, Caching and Batching
4. Breaking Changes
5. Code Generation Patterns

Consuming your GraphQL server

Apollo Client

"Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. Use it to fetch, cache, and modify application data, all while automatically updating your UI."

https://www.apollographql.com/docs/react/

TL;DR: a set of tools that helps connect your front-end to a GraphQL Server*

*Kinda, but actually a lot more

What does an Apollo client set-up look like?

The Client

// client.js
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

export const client = new ApolloClient({
  uri: 'https://graphqlserveraddress.com',
  cache: new InMemoryCache()
});

client
  .query({
    query: gql`
      query locations {
        name
        countryCode
      }
    `
  })
  .then(result => console.log(result));

Connecting it to your app

// index.js
import React from 'react';
import { render } from 'react-dom';
import { ApolloProvider } from '@apollo/client';

import { client } from './client'
import JobLocations from './JobLocations'

const App = () => {
  return (
    <ApolloProvider client={client}>
      <h2>My first Apollo app 🚀</h2>
      <JobLocations />
    </ApolloProvider>
  );
}

render(<App />, document.getElementById('root'));

Requesting data

// JobLocations.js
import { useQuery, gql } from '@apollo/client';

const LOCATIONS = gql`
  query locations {
    name
    countryCode
  }
`;

const JobLocations = () => {
  const { loading, error, data } = useQuery(LOCATIONS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  return data.locations.map(({ name, countryCode }) => (
    <div>
      <p>{name}, {countryCode}</p>
    </div>
  ));
}

Requesting/Pushing data

// Fetching data immediately 
// useQuery
const { loading, error, data } = useQuery(LOCATIONS);

// Fetching data manually
// useLazyQuery
const [getLocation, { loading, error, data }] = useLazyQuery(LOCATION);
getLocation({ variables: { locationId: 123 }})

// Mutating data 
// useMutation
const [createQuestionnaire, { loading, error, data }] = useMutation(CREATE_QUESTIONNAIRE)
createQuestionnaire({ variables: { name: 'Product Questions', ... }})

// Streaming data, requires transport layer
// useSubscription
const { loading, error, data } = useSubscription(CANDIDATE_SUBSCRIPTION)

Managing the cache

Apollo Client

Cache

GraphQL Server

Queries local and remote fields

Calculates local fields

Queries remote fields

Resolves remote fields

Returns remote fields

Caches remote fields

Returns ALL fields

Time passes...

Queries local and remote fields

Calculates local fields

Fetches remote fields (now cached)

Returns ALL fields

How local state caching is done at a high level

import { InMemoryCache, ApolloClient } from '@apollo/client';

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

Data normalisation for cache

1.The cache generates a unique ID for every identifiable object included in the response.

{
  "location": {
    "id": "seekAnz:location:seek:2FqwWaaMV",
    "__typename": "Location"
    // Generates an of of "Location:seekAnz:location:seek:2FqwWaaMV"
  }
}
 

Data normalisation for cache

2. The cache stores the objects by ID in a flat lookup table.

Data normalisation for cache

3. Whenever an incoming object is stored with the same ID as an existing object, the fields of those objects are merged.

Gotchas for cache

Once cached... it won't re-fetch from the server by default

query {
  branding(id: '5') {
    id
    name
  }
}
// Generated ID: 'Branding:5'

// Result
{
  id: '5',
  name: 'Primary Brand'
}

// Background task updates DB record
// Name: 'Primary Brand' -> 'Brand 1'

// Re-call result
{
  id: '5',
  name: 'Primary Brand'
}

Caching caches...

mutation {
  updateBranding(id: '5', name='Brand 1') {
    id
    name
  }
}

query {
  branding(id: '5') {
    id
    name
  }
}

// Result
{
  id: '5',
  name: 'Brand 1'
}

Automatic cache updates

const client = new ApolloClient({
  link: concat(authMiddleware, httpLink),
  cache: new InMemoryCache(),
  defaultOptions: {
    query: {
      fetchPolicy: 'no-cache',
    }
  },
})

//...

const { loading, error, data } = useQuery(GET_BRANDINGS, {
  fetchPolicy: "network-only"
});

// cache-first - DEFAULT
// cache-only
// cache-and-network
// network-only
// no-cache
// standby

Disabling Cache

https://www.apollographql.com/docs/react/data/queries/#supported-fetch-policies

Refetching from remote

1. View questionnaires
2. Create a questionnaire
3. Return to questionnaires
4. Where is it!?

query {
  questionnaires {
    id
    name
  }
}

// Result
[
  {
    id: '1',
    name: 'Product questionnaire'
  },{
    id: '2',
    name: 'Tech questionnaire'
  }
]

Refetching from remote

const CREATE_QUESTIONNAIRE = gql`
  mutation {
    createQuestionnaire(name: 'UX questionnaire') {
      id
      name
    }
  }
`

const [createQuestionnaire] = useMutation(CREATE_QUESTIONNAIRE)

createQuestionnaire({
  refetchQueries: ['questionnaires']
})

// query {
//   questionnaires {
//     id
//     name
//   }
// }

Refetching from remote

Managing local state

At its core, Apollo Client is a state management library that happens to use GraphQL to interact with a remote server.

https://www.apollographql.com/docs/react/local-state/local-state-management/

Application state, using local-only fields and reactive variables

local-only:
Fetching local application state via the same graphQL interface

Local-only fields

// client.js
import { ApolloClient, InMemoryCache } from '@apollo/client';
import { gql } from '@apollo/client';

export const cache = new InMemoryCache()

export const client = new ApolloClient({
  uri: 'https://graphqlserveraddress.com',
  cache
});

Local-only fields

import { useQuery } from '@apollo/client';
import { cache } from './client'

const IS_LOGGED_IN = gql`
  query IsUserLoggedIn {
    isLoggedIn @client
  }
`;

cache.writeQuery({
  query: IS_LOGGED_IN,
  data: {
    isLoggedIn: !!localStorage.getItem("token"),
  },
});

const App = () => {
  const { data } = useQuery(IS_LOGGED_IN);
  return data.isLoggedIn ? <Pages /> : <Login />;
}

Reactive variables:
State containers outside of cache. Automagically updates active queries on change.

 

Reactive variables

import { makeVar } from '@apollo/client';

const isLoggedIn = makeVar(false);

// Output: false
console.log(isLoggedIn());

isLoggedIn(true);

// Output: true
console.log(isLoggedIn());

Reactive variables

// ...

cache.writeQuery({
  query: IS_LOGGED_IN,
  data: {
    isLoggedIn: isLoggedIn(),
  },
});

On update of the reactive variable Apollo Client notifies every active query that includes the isLoggedIn field.

Reactive variables

// ...

import { useReactiveVar } from '@apollo/client';

const User = () => {
  const isLoggedIn = useReactiveVar(isLoggedIn);
  return isLoggedIn ? <Pages /> : <Login />;
}

Client-side State with Apollo

Introduction to GraphQL

Client-side State with Apollo

By Aaron Vanston

Client-side State with Apollo

  • 231