Why GraphQL Changes Front-End Data Handling
GraphQL’s core advantage over REST is that the client declares exactly which fields it needs and receives precisely that shape in response. Nested data can be pulled in a single request by extending the query, rather than chaining multiple endpoints. This eliminates over-fetching and makes responses predictable. On the client, Apollo Client provides the tooling to define, execute, and cache those queries — and can also serve as a local state manager, reducing the need for a separate library like Redux.
Beyond basic querying, Apollo supports real-time UI updates through several mechanisms: directly writing to the cache after a mutation, subscribing to server changes, and optimistically rendering data before the server responds. The project setup revolves around a React app with Apollo Client installed; the associated repos provide complete examples for cache updates, subscriptions, and local state management.
Direct Cache Updates After Mutations
When a user submits a form to add data — say, a new pet — the server mutation succeeds, but the displayed list may not refresh until the page reloads. The update function on the useMutation hook solves this by letting you read and write the cached query result (ALL_PETS) immediately after the mutation fires.
In practice, you pass an update callback to useMutation that receives the cache and the mutation result. Inside it, you read the existing ALL_PETS query from the cache, append the new pet from the mutation response, and write that list back. Because the cache is the source of truth for active queries, all subscribed components re-render with the new data instantly. This keeps the client cache consistent with the server without a full refetch.
Subscriptions for Server-Driven Updates
Subscriptions in GraphQL resemble queries, but instead of a one-off fetch they maintain a live connection to the server, pushing new data whenever the subscribed event occurs. To enable this in Apollo Client, install the subscription transport:
npm install subscriptions-transport-ws
Then in index.js, import the WebSocket link and add it to the Apollo Client configuration:
import { WebSocketLink } from "@apollo/client/link/ws";
//setting up our web sockets using WebSocketLink
const link = new WebSocketLink({
uri: `ws://localhost:4000/`,
options: {
reconnect: true,
},
});
const client = new ApolloClient({
link,
uri: "http://localhost:4000",
cache: new InMemoryCache(),
});
The uri here points to your GraphQL endpoint. Within a component, define the subscription operation instead of a query, and consume its result with the useSubscription hook rather than useQuery. Because the subscription stays open, any update from the server automatically triggers a re-render with the freshest data.
Optimistic UI
Optimistic UI goes a step further than cache updates: rather than waiting for the mutation response at all, it predicts the server result and renders it immediately. This makes interactions feel instantaneous.
When the mutation is called, the Apollo cache stores an optimistic version of the new object, separate from the canonical cached list. This preserves data integrity in case the prediction is wrong. Active queries that include the affected list are notified and re-render with the optimistic data — all without a network request. Once the server returns the actual result, the cache discards the optimistic copy and replaces it with the confirmed value, triggering a second render cycle. If the server response matches the optimistic response, the whole process is invisible to the user.
import React, { useState } from "react";
import gql from "graphql-tag";
import { useQuery, useMutation } from "@apollo/client";
import Loader from "./Loader";
import PetSection from "./PetSection";
//We use ALL_PET to send our nested queries to the server
const ALL_PETS = gql`
query AllPets {
pets {
id
name
type
img
}
}
`;
//We use NEW_PET to handle our mutations
const NEW_PET = gql`
mutation CreateAPet($newPet: NewPetInput!) {
addPet(input: $newPet) {
id
name
type
img
}
}
`;
function OptimisticPets() {
//We use useQuery to handle the ALL_PETS response and assign it to pets
const pets = useQuery(ALL_PETS);
//We use useMutation to handle mutations and updating ALL_PETS.
const [createPet, newPet] = useMutation(NEW_PET
, {
update(cache, {data: {addPet}}) {
const allPets = cache.readQuery({query: ALL_PETS})
cache.writeQuery({
query: ALL_PETS,
data: {pets: [addPet, ...allPets.pets]}
})
}
});;
const [name, setName] = useState("");
const type = `DOG`;
//Handles mutation and creates the optimistic response
const onSubmit = (input) => {
createPet({
variables: { newPet: input },
optimisticResponse: {
__typename: 'Mutation',
addPet: {
__typename: 'Pet',
id: Math.floor(Math.random() * 1000000) + '',
type: "CAT",
name: input.name,
img: 'https://via.placeholder.com/300',
}
}
});
};
//Here's our submit triggers the onSubmit function
const submit = (e) => {
e.preventDefault();
onSubmit({ name, type });
};
//returns the loading the component when the data is still loading
if (pets.loading ) {
return <Loader />;
}
//loops through the pets and displays them in the PetSection component
const petsList = pets.data.pets.map((pet) => (
<div className="col-xs-12 col-md-4 col" key={pet.id}>
<div className="box">
<PetSection pet={pet} />
</div>
</div>
));
return (
<div>
<form onSubmit={submit}>
<input
className="input"
type="text"
placeholder="pet name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<button type="submit" name="submit">
add pet
</button>
</form>
<div>
{petsList}
</div>
</div>
);
}
export default OptimisticPets;
Apollo as a Local State Manager
Apollo’s caching layer can also handle client-only state alongside remote data. The approach uses client-side schemas and resolvers to define and populate local fields.
For example, extend an existing User type with an additional integer field, such as height, and attach a resolver to supply its value. Define this in a dedicated module (e.g., Client.js) and then pass the resulting client instance into your app’s root component:
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloLink } from 'apollo-link'
import { HttpLink } from 'apollo-link-http'
import { setContext } from 'apollo-link-context'
import gql from 'graphql-tag'
//Extending the User type
const typeDefs = gql`
extend type User {
height: Int
}
`
//Declaring our height inside our resolvers within the client-side
const resolvers = {
User : {
height() {
return 35
}
}
}
const cache = new InMemoryCache()
const http = new HttpLink({
uri: 'http://localhost:4000/'
})
const link = ApolloLink.from([
http
])
const client = new ApolloClient({
link,
cache,
typeDefs,
resolvers
})
export default client
client.jsimport client from "./client"
import {
ApolloProvider,
} from "@apollo/client";
//importing our client.js file into ApolloProvider
ReactDOM.render(
<ApolloProvider client={client}>
<Routing />
</ApolloProvider>,
document.getElementById("root")
);
index.js
Inside a component, you can query this local field within the same operation as remote data, using the @client directive to tell Apollo not to fetch it from the server:
const ALL_PETS = gql`
query AllPets {
pets {
id
name
type
img
owner {
id
height @client
}
}
}
`;
The results are combined and exposed through the single useQuery hook, merging server and client state seamlessly.
Reusable Queries with Fragments
If multiple components need the same set of fields, repeating the query definition is error-prone. Fragments offer a way to define those fields once and reuse them. You declare a fragment on a type — for example, PetFields on Pet — and then spread it into any query or mutation that needs those fields:
const DUPLICATE_FIELD = gql`
fragment PetFields on Pet {
id
name
type
img
}
`
const ALL_PETS = gql`
query AllPets {
pets {
...PetFields
}
}
${DUPLICATE_FIELD}
`;
const NEW_PET = gql`
mutation CreateAPet($newPet: NewPetInput!) {
addPet(input: $newPet) {
...PetFields
}
}
${DUPLICATE_FIELD}
`;
Conditional Logic with Directives
Apollo directives add conditional logic directly to queries. The built-in @skip and @include directives take a boolean variable to control whether a field or fragment is resolved. @skip omits the field when the condition is true; @include includes it when the condition is true:
const ALL_PETS = gql`
query AllPets($name: Boolean!){
pets {
id
name @skip: (if: $name)
type
img
}
}
`;
Here $name is a boolean variable passed when invoking the query; if true, the name field is skipped. The @deprecated directive works in schemas to retire fields (optionally with a reason). Third-party libraries expand this directive set for more complex scenarios.
Shaping Responses with GraphQL Lodash
GraphQL Lodash brings lodash-style transformations into queries. Rather than looping through arrays in client code, you can ask the server to shape the response more usefully.
A plain query for film titles returns each title as an object in an array:
films {
title
}"films": [
{
"title" : "Prremier English"
},
{
"title" : "There was a country"
},
{
"title" : "Fast and Furious"
}
{
"title" : "Beauty and the beast"
}
]
Using the _map directive, the query flattens the result into a single array of title strings:
films @_(map: "title") {
title
}"films": [
"Premier English",
"There was a country",
"Fast and Furious",
"Beauty and the beast"
]
Similarly, the _keyBy directive turns an array of objects into a dictionary keyed by a specified field. A simple query listing people:
people {
name
age
gender
}"people" : [
{
"name": "James Walker",
"age": "19",
"gender": "male"
},
{
"name": "Alexa Walker",
"age": "19",
"gender": "female"
},
]
With the _keyBy directive applied, the response is keyed by the person’s name:
people @_(keyBy: "name") {
name
age
gender
}"people" : [
"James Walker" : {
"name": "James Walker",
"age": "19",
"gender": "male"
}
"Alexa Walker" : {
"name": "Alexa Walker",
"age": "19",
"gender": "female"
}
]
These directives produce more compact, ready-to-use data structures, reducing transformation work on the front end.
A working codebase for the cache-update, local-state, and fragment examples is available on GitHub; a separate repo covers subscriptions. For a full walkthrough of GraphQL with React, Scott Moss’s tutorial is a recommended starting point.



