I wanted to figure out how I might be able to take advantage of Apollo Client caching while using RSC. I came up with this
approach, which is rather rudimentary, but seems like a good starting point. It is taking advantage of the new localStorage
API added in Node 22 https://nodejs.org/api/globals.html#localstorage
I built up a new query
method which wraps the existing query and adds in ability to pass in an identifier (likely a user ID
or something of that matter) to ensure that caches don't overlap.
// rsc-client.ts
import {
ApolloClient,
HttpLink,
InMemoryCache,
from,
NormalizedCacheObject,
ApolloQueryResult,
QueryOptions,
} from "@apollo/client";
import { onError } from "@apollo/client/link/error";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support";
export const APOLLO_STATE_PROP_NAME = "__APOLLO_STATE__";
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`),
);
if (networkError) console.log(`[Network error]: ${networkError}`);
});
const httpLink = new HttpLink({
uri: "http://localhost:6001/api/graphql",
});
export const { getClient } = registerApolloClient(() => {
return new ApolloClient({
cache: new InMemoryCache(),
link: from([errorLink, httpLink]),
});
});
function writeCache(identifier: string, client: ApolloClient<NormalizedCacheObject>) {
const existingCache = client.extract();
const stringCache = JSON.stringify(existingCache);
// Using Node 22's localStorage
localStorage.setItem(`${APOLLO_STATE_PROP_NAME}-${identifier}`, stringCache);
}
function restoreCache(identifier: string): NormalizedCacheObject {
// Using Node 22's localStorage
const cache = localStorage.getItem(`${APOLLO_STATE_PROP_NAME}-${identifier}`);
return cache ? JSON.parse(cache) : null;
}
export function initializeApollo(identifier: string) {
const client = getClient();
const cache = restoreCache(identifier);
if (cache) {
client.cache.restore(cache);
}
return client;
}
export async function query<T = any>(identifier: string, options: QueryOptions): Promise<ApolloQueryResult<T>> {
const client = initializeApollo(identifier);
const result = await client.query(options);
writeCache(identifier, client);
return result;
}
// page.tsx
import { gql } from "graphql-tag";
import { query } from "@/graphql/rsc-client";
import { Book } from "@/components/book";
const BOOK_QUERY = gql`
query BookQuery($isbn: String!) {
book(isbn: $isbn) {
id
title
dateAdded
isbn
author {
name
}
}
}
`;
type Props = {
params: {
isbn: string;
};
};
export const dynamic = "force-dynamic";
export default async function BooksPage({ params: { isbn } }: Props) {
const { data, loading, error } = await query("cache-testing", {
query: BOOK_QUERY,
variables: { isbn: isbn },
fetchPolicy: "cache-only",
});
if (loading) return <h3 className="text-black text-2xl mb-2">Query Loading</h3>;
if (error) return <h3 className="text-black text-2xl mb-2">Query Error! {error.message}</h3>;
if (!data || !data.book) return <p className="text-black">No data available</p>;
return (
<>
<h1 className="text-black text-2xl mb-2">200ms delayed Book</h1>
<Book book={data.book} />
</>
);
}