GraphQL - Caching
GraphQL - Caching with Apollo Client
Section titled “GraphQL - Caching with Apollo Client”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.
How InMemoryCache Works: Normalization
Section titled “How InMemoryCache Works: Normalization”InMemoryCache normalizes your data before storing it. Normalization means:
- Unique Identification: Each object with a
__typenameand a unique identifier (idor_idby default, or a custom key viakeyFieldsintypePolicies) 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
Studentobject having acollegefield), 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.
Fetch Policies
Section titled “Fetch Policies”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: Likenetwork-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.
Setting up the Server
Section titled “Setting up the Server”Ensure you have a GraphQL server running (similar to previous examples) with the following schema and resolvers:
Schema (schema.graphql or in server.js)
Section titled “Schema (schema.graphql or in server.js)”#graphqltype Query { students: [Student] # Field to get the current server time serverTime: String}
type Student { id: ID! firstName: String! lastName: String}Resolvers (resolvers.js)
Section titled “Resolvers (resolvers.js)”// resolvers.js// Assume db.students.list() exists from previous examplesimport { 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;Run the Server
Section titled “Run the Server”Start your GraphQL server (e.g., npm run dev).
Setting up the React Client
Section titled “Setting up the React Client”Create a React application if you haven’t already.
Step 1: Install Dependencies
Section titled “Step 1: Install Dependencies”npm install @apollo/client graphql react-router-domWe add react-router-dom for navigation between routes.
Step 2: Configure Apollo Client & Router
Section titled “Step 2: Configure Apollo Client & Router”Set up the client and router in your main entry file (e.g., 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 Clientconst 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.
import React from 'react';import { Routes, Route, Link } from 'react-router-dom';import StudentsPage from './StudentsPage'; // We'll create this nextimport HomePage from './HomePage'; // We'll create this nextimport './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;Step 4: Create HomePage.jsx
Section titled “Step 4: Create HomePage.jsx”// src/HomePage.jsximport 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.jsximport 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;Step 6: Run and Observe Caching
Section titled “Step 6: Run and Observe Caching”-
Start your React development server (e.g.,
npm run dev). -
Open the app in your browser and open the developer console.
-
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.studentsandQuery.serverTime. The browser will log the component rendering and the final data. Note the ‘Server Time’ displayed. -
Navigate to ‘Home’: Go back to the home page.
-
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.
-
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.
Experimenting with Fetch Policies
Section titled “Experimenting with Fetch Policies”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.
Cache Configuration and Control
Section titled “Cache Configuration and Control”typePolicies: Allows fine-grained control over how different types are cached, including defining custom key fields (ifidisn’t the unique identifier) and field-level read/merge functions.- Cache Eviction/Garbage Collection:
InMemoryCacheautomatically removes disconnected objects. You can configure limits or manually evict data. - Direct Cache Access: Use
client.readQuery,client.writeQuery,client.readFragment,client.writeFragment, andclient.cache.modifyfor imperative cache manipulation (often used in mutationupdatefunctions).
Mastering Apollo Client’s cache is key to building high-performance GraphQL applications. Refer to the official documentation for advanced configurations.