apollographql / apollo-client-nextjs

Apollo Client support for the Next.js App Router
https://www.npmjs.com/package/@apollo/experimental-nextjs-app-support
MIT License
358 stars 25 forks source link

Proper way to use with Next SSR caching - Always seeing loading state / data not caching #291

Open sgup opened 2 weeks ago

sgup commented 2 weeks ago

Hi, first of all thanks for this amazing library.

I'm trying to figure out the proper use for rendering & caching with SSR/Next. I'm using fetchOptions: { cache: 'force-cache' } in the SSR Apollo Wrapper. However I'm still seeing my graphql server being hit for every request, and it always shows the loading page for a second or if i disable JS (testing on the deploy).

Folder structure:

- app/blog/[id]/page.tsx
  - server component (no 'use client')
  - uses an apollo RSC client here for generateMetadata (no fetchOptions defined)
  - return <BlogPage id={id} />
  - export const revalidate = 86400;

- app/blog/[id]/loading.tsx
  - server component
  - loading screen

- app/blog/[id]/BlogPage.tsx
  - client component 'use client'
  - const { data } = useSuspenseQuery(GetGuideDocument, {
      variables: {
        guideId,
      },
      fetchPolicy: 'cache-and-network',
      context: {
        fetchOptions: {
          cache: 'force-cache',
        },
      },
    });

Full SSR:

'use client';
declare module '@apollo/client' {
  export interface DefaultContext {
    token?: string;
  }
}

import { ApolloLink, HttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import {
  ApolloNextAppProvider,
  NextSSRInMemoryCache,
  NextSSRApolloClient,
  SSRMultipartLink,
} from '@apollo/experimental-nextjs-app-support/ssr';
import typePolicies from './typePolicies';

const uri = `${process.env.NEXT_PUBLIC_GRAPHQL_URL}/graphql`;

// have a function to create a client for you
function makeClient() {
  const authLink = setContext(async (_, { headers, token }) => {
    return {
      headers: {
        ...headers,
        ...(token ? { authorization: `Bearer ${token}` } : {}),
      },
    };
  });

  const httpLink = new HttpLink({
    // this needs to be an absolute url, as relative urls cannot be used in SSR
    uri,
    // you can disable result caching here if you want to
    // (this does not work if you are rendering your page with `export const dynamic = "force-static"`)
    fetchOptions: { cache: 'force-cache' },
  });

  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache({
      typePolicies,
    }),
    link:
      typeof window === 'undefined'
        ? ApolloLink.from([
            new SSRMultipartLink({
              stripDefer: true,
            }),
            authLink.concat(httpLink),
          ])
        : authLink.concat(httpLink),
  });
}

// you need to create a component to wrap your app in
export function ApolloWrapper({ children }: React.PropsWithChildren) {
  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      {children}
    </ApolloNextAppProvider>
  );
}

Full RSC

import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { registerApolloClient } from '@apollo/experimental-nextjs-app-support/rsc';

const { getClient } = registerApolloClient(() => {
  return new ApolloClient({
    cache: new InMemoryCache(),
    link: new HttpLink({
      uri: process.env.NEXT_PUBLIC_GRAPHQL_URL + '/graphql',
      // you can disable result caching here if you want to
      // (this does not work if you are rendering your page with `export const dynamic = "force-static"`)
      // fetchOptions: { cache: "no-store" },
    }),
  });
});

export default getClient;

Thanks, any help much appreciated.

phryneas commented 2 weeks ago

This looks to me like you're forwarding the option the Next.js fetch cache correctly. Unfortunately, beyond that it's more of a Next.js question and I don't really know how you could best debug this, I'm sorry.

sgup commented 1 week ago

Would there be a way to do the initial fetch using RSC (which seems to respect nextjs caching), passing the data to the client component, and then "patching-in" that cache into the client-side apollo client?

Essentially a way to auto-load the rsc-client cache into the browser apollo client cache would be amazing.

sgup commented 1 week ago

I seem to have answered my own question. Using this approach Apollo Docs: Rehydrating the client-side cache, and passing in the RSC Client's cache into the <ApolloWrapper> from the SSR Client (can maybe change it to a regular client?) in app/layout.tsx.

Usage:

phryneas commented 1 week ago

Please don't do that, that cannot work correctly in all situations and is not supported! The App router runs in streaming SSR, worst case RSC, SSR and the browser run simulateneously, it's even possible that you'd end up starting the same request mutliple times or overriding data in the browser. We will soon ship support for that in this package, please don't try this before the next version (see #258).

The documentation you linked to is only valid for non-streaming renderToString SSR.

sgup commented 5 days ago

I see, ok! looking forward to the new support, thank you!

sgup commented 5 days ago

Actually i think what im doing right now isn't causing multiple concurrent queries, since I'm forcing static on the pages.

// page.tsc
// fetching data here with rscClient to cache for 24 hours, renders the page initially instantly without any visible loading state.
export const dynamic = 'force-static';
export const revalidate = 86400; // 24 hours

and then bootstrapping this client's cache into the browser cache, but doing a fresh query on the clientside to update the data.