Skip to content

GraphQL - Caching

Caching is one of the most powerful features of GraphQL client libraries like Apollo Client. By storing previously fetched data locally, the client can avoid redundant network requests, leading to faster UI updates, reduced server load, and a better user experience, especially on slow or unreliable networks.

Apollo Client’s caching is primarily handled by InMemoryCache.

InMemoryCache normalizes your data before storing it. Normalization means:

  • Unique Identification: Each object with a __typename and a unique identifier (id or _id by default, or a custom key via keyFields in typePolicies) is stored only once in a flat lookup table.
  • Storing by ID: The cache stores objects indexed by their unique cache ID (typically ${__typename}:${id}).
  • Replacing Objects with References: When one object references another (e.g., a Student object having a college field), the cache stores a reference (the cache ID) to the related object instead of nesting the full object data.
  • Automatic Updates: When an object in the cache is updated (e.g., via a mutation result), any query result that references that object will automatically see the updated data without needing to refetch.

This normalization allows Apollo Client to efficiently manage potentially overlapping or deeply nested query results and keep the UI consistent.

Apollo Client’s behavior regarding the cache and network requests is controlled by fetch policies. You can set a default policy when initializing the client or specify one per query using the fetchPolicy option in useQuery.

Common fetch policies:

  • cache-first (Default): Checks the cache first. If all requested data is present, it’s returned from the cache. Otherwise, a network request is made. Updates the cache after the network request.
  • cache-and-network: Checks the cache first (returns data immediately if available). Then, always makes a network request. Updates the cache and potentially the UI again if network data differs. Good for fast initial load + background updates.
  • network-only: Always makes a network request, ignoring the cache for reading initially. Updates the cache after the request.
  • cache-only: Only checks the cache. Never makes a network request. Throws an error if data isn’t in the cache.
  • no-cache: Like network-only, but doesn’t even update the cache after the network request completes.

Choosing the right fetch policy depends on how fresh the data needs to be for a particular query.

Illustration: Demonstrating Caching Behavior

Section titled “Illustration: Demonstrating Caching Behavior”

We’ll create a simple React application with two routes: ‘Home’ and ‘Students’. The ‘Students’ route will fetch data using Apollo Client. We’ll include a server timestamp in the query to observe when network requests are actually made versus when data is served from the cache.

Ensure you have a GraphQL server running (similar to previous examples) with the following schema and resolvers:

#graphql
type Query {
students: [Student]
# Field to get the current server time
serverTime: String
}
type Student {
id: ID!
firstName: String!
lastName: String
}
// resolvers.js
// Assume db.students.list() exists from previous examples
import { db } from './db.js';
const resolvers = {
Query: {
students: () => {
console.log("Resolving: Query.students");
return db.students.list();
},
serverTime: () => {
const now = new Date();
console.log(`Resolving: Query.serverTime at ${now.toLocaleTimeString()}`);
return now.toISOString(); // Return ISO string for consistency
},
},
};
export default resolvers;

Start your GraphQL server (e.g., npm run dev).

Create a React application if you haven’t already.

npm install @apollo/client graphql react-router-dom

We add react-router-dom for navigation between routes.

Set up the client and router in your main entry file (e.g., src/main.jsx).

src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import './index.css';
// Configure Apollo Client
const client = new ApolloClient({
uri: 'http://localhost:9000/graphql', // Your server URL
cache: new InMemoryCache(),
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<ApolloProvider client={client}>
<BrowserRouter> { /* Use BrowserRouter or HashRouter */ }
<App />
</BrowserRouter>
</ApolloProvider>
</React.StrictMode>
);

Step 3: Create Components and Routing (App.jsx)

Section titled “Step 3: Create Components and Routing (App.jsx)”

Set up basic routing and navigation.

src/App.jsx
import React from 'react';
import { Routes, Route, Link } from 'react-router-dom';
import StudentsPage from './StudentsPage'; // We'll create this next
import HomePage from './HomePage'; // We'll create this next
import './App.css';
function App() {
return (
<div>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/students">Students</Link></li>
</ul>
</nav>
<hr />
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/students" element={<StudentsPage />} />
</Routes>
</main>
</div>
);
}
export default App;
// src/HomePage.jsx
import React from 'react';
function HomePage() {
const clientTime = new Date().toLocaleTimeString();
return (
<div>
<h2>Home Page</h2>
<p>Welcome! This page doesn't fetch GraphQL data.</p>
<p>Current Client Time: {clientTime}</p>
</div>
);
}
export default HomePage;

Step 5: Create StudentsPage.jsx (Fetches Data)

Section titled “Step 5: Create StudentsPage.jsx (Fetches Data)”

This component will use useQuery to fetch students and the server time.

// src/StudentsPage.jsx
import React from 'react';
import { useQuery, gql } from '@apollo/client';
const GET_STUDENTS_AND_TIME = gql`
query GetStudentsAndTime {
serverTime
students {
id
firstName
}
}
`;
function StudentsPage() {
console.log('Rendering StudentsPage...');
// Using default fetchPolicy: 'cache-first'
const { loading, error, data, refetch } = useQuery(GET_STUDENTS_AND_TIME, {
notifyOnNetworkStatusChange: true, // Show loading state even on refetch
});
const clientTime = new Date().toLocaleTimeString();
console.log('Query State:', { loading, error, data });
return (
<div>
<h2>Students List</h2>
<p>Current Client Time: {clientTime}</p>
<button onClick={() => refetch()} disabled={loading}>
Refetch Data
</button>
{loading && <p>Loading data from server...</p>}
{error && <p>Error fetching data: {error.message}</p>}
{data && (
<div>
<h3>Data fetched at Server Time: {new Date(data.serverTime).toLocaleTimeString()}</h3>
<h4>Students:</h4>
<ul>
{data.students.map(s => (
<li key={s.id}>{s.firstName}</li>
))}
</ul>
</div>
)}
</div>
);
}
export default StudentsPage;
  1. Start your React development server (e.g., npm run dev).

  2. Open the app in your browser and open the developer console.

  3. Navigate to ‘Students’: The first time, you’ll see ‘Loading…’ briefly. Check the console logs in both your server terminal and the browser console. The server should log that it resolved Query.students and Query.serverTime. The browser will log the component rendering and the final data. Note the ‘Server Time’ displayed.

  4. Navigate to ‘Home’: Go back to the home page.

  5. Navigate back to ‘Students’: This time, the data should appear instantly without a ‘Loading…’ message (unless the data was garbage collected by the cache, which is unlikely for this simple case). Crucially, the ‘Server Time’ displayed should be the same as the first time you visited. Check the server logs – you should not see new logs for resolving the query. This indicates the data was served from the Apollo Client cache.

  6. Click ‘Refetch Data’: You should see ‘Loading…’ again briefly. The server logs will show the query being resolved again. The ‘Server Time’ displayed on the page will update to the new time.

Modify the useQuery in StudentsPage.jsx to try different fetch policies:

// Try: fetchPolicy: 'network-only'
const { loading, error, data } = useQuery(GET_STUDENTS_AND_TIME, {
fetchPolicy: 'network-only'
});
// Try: fetchPolicy: 'cache-and-network'
// const { loading, error, data } = useQuery(GET_STUDENTS_AND_TIME, {
// fetchPolicy: 'cache-and-network'
// });

Observe how the behavior changes when navigating back and forth between ‘Home’ and ‘Students’. network-only will always show ‘Loading…’ and fetch fresh data. cache-and-network will load instantly from cache and trigger a background refetch, potentially updating the server time shortly after initial render.

  • typePolicies: Allows fine-grained control over how different types are cached, including defining custom key fields (if id isn’t the unique identifier) and field-level read/merge functions.
  • Cache Eviction/Garbage Collection: InMemoryCache automatically removes disconnected objects. You can configure limits or manually evict data.
  • Direct Cache Access: Use client.readQuery, client.writeQuery, client.readFragment, client.writeFragment, and client.cache.modify for imperative cache manipulation (often used in mutation update functions).

Mastering Apollo Client’s cache is key to building high-performance GraphQL applications. Refer to the official documentation for advanced configurations.