GraphQL Conventions Worth Knowing
GraphQL is a declarative query language backed by a formal specification. It uses a strong type schema for your API, which lets clients request exactly the data they need in the shape they want. A few conventions are essential before we start working with Apollo Client:
- Operations. Every action in GraphQL is an operation. Queries fetch data (read-only), mutations create, update, or delete data, and subscriptions push data from the server to clients when events occur, usually over WebSockets. In this article, we'll work only with queries and mutations.
- Operation names. Client-side queries and mutations should have unique, descriptive names.
- Variables and arguments. Operations can accept arguments like functions. You define variables within an operation and pass their values at runtime from the client.
- Aliasing. You can rename verbose or vague field names with clearer names for the UI. Aliasing helps avoid conflicts when several fields return with the same name.
Why Client-Side GraphQL?
In REST, a client fetches from endpoints that return fixed structures. That leads to overfetching — downloading more data than the app needs — or underfetching, where you must make additional requests to get what you actually require. GraphQL solves this by letting the client send a single query describing precisely which fields are needed. The server responds with JSON matching that exact shape.
Client-side GraphQL is the infrastructure on the front end that interfaces with a GraphQL server. It manages several jobs for you:
- Data management. You can send queries and mutations without constructing HTTP requests by hand, freeing time to build the application itself.
- Caching. Fetched data is stored and retrieved intelligently so duplicate resources aren't re-requested. The cache can recognize when two resources are the same, which is valuable in a complex app.
- Optimistic UI. This technique simulates a mutation's result and updates the UI before the server responds. When the real response arrives, the simulated data is replaced with actual data, keeping the interface consistent.
Introduction to Apollo Client
Apollo Client is a community-driven GraphQL client for JavaScript and native platforms. It is interoperable with other frameworks (React, Angular, Vue.js) and brings a strong set of features to the table: a state-management tool called Apollo Link, a zero-config caching system, declarative data fetching, pagination support, and Optimistic UI. Apollo Client manages state for both server-fetched data and locally created data. It can also coexist with other state-management tools like Redux without conflict.
Key built-in capabilities worth highlighting:
- Caching. Apollo Client caches data on the fly with no extra configuration.
- Optimistic UI. The UI temporarily renders the final state of an operation while the mutation is in flight; the real server data replaces it when complete.
- Pagination. The
fetchMorefunction from theuseQueryhook handles the technical work of fetching lists, either in batches or in one go.
Building the Pet Shop App
Our project is a simple pet shop. We will implement four features: fetching pets from the server, creating a pet with a name, type, and image, applying Optimistic UI, and using pagination to segment data.
To get started:
- Clone the repository on the
starterbranch. - Install the Apollo Client Developer Tools extension for Chrome.
- Run
npm installto get dependencies. - Run
npm run appto start the client. - Run
npm run serverto start the back-end server.
The app opens at a configured port (often https://localhost:1234/). You will notice no pets are displayed yet — that functionality is what we'll build next.
Once the Apollo Client Developer Tools extension is installed, open the developer tools and click the Apollo tray icon. You should see the extension's panel, which includes the GraphQL Playground for writing and testing queries and mutations before using them in the app.
Querying With Apollo Client
Set up the Apollo Client instance in client/src/client.js. This module creates the client, connects it to the API, and exports it for use throughout the application.
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'
const link = new HttpLink({ uri: 'https://localhost:4000/' })
const cache = new InMemoryCache()
const client = new ApolloClient({
link,
cache
})
export default client
Here is what each piece does:
ApolloClientis the core wrapper that handles HTTP communication, data caching, and UI updates.InMemoryCacheprovides the normalized data store Apollo Client uses to manage cached data.HttpLinkis the standard network interface for GraphQL requests. It works as middleware and fetches results from the GraphQL server. It replaces the need for libraries such asAxiosorwindow.fetch.
The link variable holds an HttpLink instance pointing to the server at https://localhost:4000/. The cache variable is a new InMemoryCache instance. The exported client wraps both in ApolloClient.
To make Apollo available across the entire component tree, wrap the app in ApolloProvider inside client/src/index.js. This provider works like React’s Context.Provider, placing the client in context so any component can access it.
import React from 'react' import ReactDOM from 'react-dom' import { BrowserRouter } from 'react-router-dom' import { ApolloProvider } from '@apollo/react-hooks' import App from './components/App' import client from './client' import './index.css' const Root = () => ( <BrowserRouter><ApolloProvider client={client}> <App /> </ApolloProvider></BrowserRouter> ); ReactDOM.render(<Root />, document.getElementById('app')) if (module.hot) { module.hot.accept() }
With the client in place, define a query in client/src/pages/Pets.js that requests only the fields you need.
import React, {useState} from 'react' import gql from 'graphql-tag' import { useQuery, useMutation } from '@apollo/react-hooks' import PetsList from '../components/PetsList' import NewPetModal from '../components/NewPetModal' import Loader from '../components/Loader'const GET_PETS = gql` query getPets { pets { id name type img } } `;export default function Pets () { const [modal, setModal] = useState(false)const { loading, error, data } = useQuery(GET_PETS); if (loading) return <Loader />; if (error) return <p>An error occured!</p>;const onSubmit = input => { setModal(false) } if (modal) { return <NewPetModal onSubmit={onSubmit} onCancel={() => setModal(false)} /> } return ( <div className="page pets-page"> <section> <div className="row betwee-xs middle-xs"> <div className="col-xs-10"> <h1>Pets</h1> </div> <div className="col-xs-2"> <button onClick={() => setModal(true)}>new pet</button> </div> </div> </section> <section><PetsList pets={data.pets}/></section> </div> ) }
The gql Tag
GraphQL operations are written as strings parsed with the gql template literal tag. This tag converts the query string into a GraphQL AST (abstract syntax tree).
- A query operation fetches pets from the server. The operation must declare its type before its name. Here the query is named
GET_PETSand requests thepetsfield with the specific subfields(id, name, type, img). Field names follow GraphQL’s camelCase convention. useQuery, imported from@apollo/react-hooks, is the React hook for executing queries. Pass theGET_PETSquery string to it.- When the component renders,
useQueryreturns an object withloading,error, anddataproperties, which are destructured to drive the UI. - The hook handles async state internally, so no
async-awaitis needed in the component.
The response properties work as follows:
loadingisfalseby default and is used to render aLoadercomponent while data is fetched.errorcaptures any request failures.datacarries the actual server response. ThePetsListcomponent receivespetsas a prop set todata.pets.
To run the full application, start the client with npm run app and the server with npm run server in separate terminals.

Creating Data With Mutations
Mutations follow the same pattern as queries with minor differences. In client/src/pages/Pets.js, add the mutation setup.
.... const GET_PETS = gql` query getPets { pets { id name type img } } `;const NEW_PETS = gql` mutation CreateAPet($newPet: NewPetInput!) { addPet(input: $newPet) { id name type img } } `;const Pets = () => { const [modal, setModal] = useState(false) const { loading, error, data } = useQuery(GET_PETS);const [createPet, newPet] = useMutation(NEW_PETS);const onSubmit = input => { setModal(false)createPet({ variables: { newPet: input } }); } if (loading || newPet.loading) return <Loader />; if (error || newPet.error) return <p>An error occured</p>;if (modal) { return <NewPetModal onSubmit={onSubmit} onCancel={() => setModal(false)} /> } return ( <div className="page pets-page"> <section> <div className="row betwee-xs middle-xs"> <div className="col-xs-10"> <h1>Pets</h1> </div> <div className="col-xs-2"> <button onClick={() => setModal(true)}>new pet</button> </div> </div> </section> <section> <PetsList pets={data.pets}/> </section> </div> ) } export default Pets
1. The mutation Operation
For create, update, or delete actions, use the mutation operation type. This example defines CreateAPet with a required argument $newPet of type NewPetInput. The ! suffix makes the variable mandatory.
2. The addPet Function
Inside the mutation, addPet accepts an input argument mapped to $newPet. The returned fields must match the query set: id, name, type, and img.
3. The useMutation Hook
useMutation is the primary hook for mutations. Call it with the NEW_PETS GraphQL string. The hook returns a tuple containing a mutate function and an object describing the mutation’s current state. The destructured function is named createPet, and the state object is newPets.
4. The createPet Function
Inside the onSubmit handler, after the modal state is set, createPet is called with variables set to { newPet: input }. The input value holds the form fields such as name and type.

Notice that the newly created pet does not appear immediately in the UI — only after a page refresh. The server data is updated, but the client view is not. The next section explains why.
Managing Cache Synchronization
The new pet is not rendered instantly because the newly created data does not align with the cache Apollo Client is using. When a mutation creates or deletes multiple entries, you must update any queries that reference those entries so the cached data matches the back-end state.
One approach is to refetch affected queries using refetchQueries. In createPet, you would add refetchQueries: [{ query: GET_PETS }]. This approach is the simplest but bypasses direct cache manipulation, which is the focus here.
The preferred approach is the update helper function. Apollo Client’s update function reads and writes directly to the cache, syncing it with the mutation’s server-side effects.
Writing to the Cache
Add the cache update logic in client/src/pages/Pets.js.
...... const Pets = () => { const [modal, setModal] = useState(false) const { loading, error, data } = useQuery(GET_PETS);const [createPet, newPet] = useMutation(NEW_PETS, { update(cache, { data: { addPet } }) { const data = cache.readQuery({ query: GET_PETS }); cache.writeQuery({ query: GET_PETS, data: { pets: [addPet, ...data.pets] }, }); }, } );.....
The update function takes two arguments. The first is the Apollo cache instance. The second is the mutation response; the data property is destructured and aliased to the addPet mutation result.
Identify the query to update — here, GET_PETS — and read its cached data. Then write back to that query by passing an object with the query set to GET_PETS and a data field containing the pets array that appends the new addPet result to the existing pet list.
With this update function in place, newly created pets appear automatically without a refresh.

Optimistic UI: Faster feedback without spinners
Loaders and spinners still have their place, but for mutations whose outcome is predictable, waiting on the network is unnecessary friction. Optimistic UI is the convention of rendering the expected result of a mutation immediately, then replacing that temporary state with the server's real response when it arrives. Apollo Client supports this pattern directly through the cache, so the UI updates synchronously while the request is still in flight.
Delaying the network for demonstration
To see the effect clearly, the example first introduces an artificial delay on the mutation link. In client/src/client.js, import setContext from apollo-link-context. The callback passed to setContext returns a promise whose setTimeout is set to 800ms, and ApolloLink.from applies that delay to the HTTP link.
import { ApolloClient } from 'apollo-client' import { InMemoryCache } from 'apollo-cache-inmemory' import { HttpLink } from 'apollo-link-http'import { setContext } from 'apollo-link-context' import { ApolloLink } from 'apollo-link' const http = new HttpLink({ uri: "https://localhost:4000/" }); const delay = setContext( request => new Promise((success, fail) => { setTimeout(() => { success() }, 800) }) ) const link = ApolloLink.from([ delay, http ])const cache = new InMemoryCache() const client = new ApolloClient({ link, cache }) export default client
Writing the optimistic response
With the delay in place, the optimistic update is configured in client/src/pages/Pets.js by adding an optimisticResponse object to the mutation call. When the user creates a pet, Apollo writes this object to the cache immediately, before the server responds.
..... const Pets = () => { const [modal, setModal] = useState(false) const { loading, error, data } = useQuery(GET_PETS); const [createPet, newPet] = useMutation(NEW_PETS, { update(cache, { data: { addPet } }) { const data = cache.readQuery({ query: GET_PETS }); cache.writeQuery({ query: GET_PETS, data: { pets: [addPet, ...data.pets] }, }); }, } ); const onSubmit = input => { setModal(false) createPet({ variables: { newPet: input },optimisticResponse: { __typename: 'Mutation', addPet: { __typename: 'Pet', id: Math.floor(Math.random() * 10000 + ''), name: input.name, type: input.type, img: 'https://via.placeholder.com/200' } }}); } .....
That object mirrors the shape of the mutation result:
__typename: Apollo injects this into queries to know the type of each entity; it uses those types to build cache identifiers. The optimistic response must set it — for the mutation namedaddPet, the type isPet.id: Since the server-generated ID isn't known yet, one can be fabricated withMath.floor.name: Taken directly frominput.name.type: Likewise set frominput.type.img: The server normally generates images, so a placeholder is used to stand in until the real URL arrives.
Once the mutation resolves, Apollo discards the optimistic entry and replaces it with the actual server response, keeping the cache consistent.

Where to go from here
Combined with pagination and the other cache features covered earlier, optimistic UI makes Apollo Client a practical choice for data-heavy React applications. The hooks API keeps the setup declarative, and the same library works with Vue and Angular if you switch frameworks later.
This walkthrough has only touched the surface. The supporting repository is available on GitHub — cloning it and adding pagination or experimenting with the cache is a reasonable next step for practicing these patterns.



