Skip to content

Instantly share code, notes, and snippets.

@jer-k
Created August 13, 2024 18:40
Show Gist options
  • Save jer-k/901c749443fcfd80c8f6b08d681d9831 to your computer and use it in GitHub Desktop.
Save jer-k/901c749443fcfd80c8f6b08d681d9831 to your computer and use it in GitHub Desktop.
Apollo Client Next RSC Cache

An approach to building an Apollo Client cache for RSC

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} />
    </>
  );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment