Skip to content

Next JS 13 (AppDir and RSC) with ApolloClient #10344

@lloydrichards

Description

@lloydrichards

Intended outcome:
After moving my app from the pages layout to the new app directory, I should be able to make simple queries to my GraphQL endpoint using apollo-client and graphql-codegen

Actual outcome:
With the new NextJS 13 server rendered components, its no longer possible to provide the apollo context at the root of the app and then useQuery() to fetch the data.

How to reproduce the issue:
I initially tried to match up the pageProps that I was using in the previous version:

config/apolloClient.ts
export const APOLLO_STATE_PROP_NAME = "__APOLLO_STATE__";

let apolloClient: ApolloClient<NormalizedCacheObject> | null = null;

type SchemaContext =
  | SchemaLink.ResolverContext
  | SchemaLink.ResolverContextFunction;

function createIsomorphicLink(_?: SchemaContext) {
  const httpLink = new HttpLink({
    uri: `${
      process.env.NODE_ENV == "production"
        ? process.env.GRAPHQL_URL_PROD
        : "http://localhost:5001/life-hive/us-central1/graphql"
    }`,
    credentials: "same-origin",
  });
  return from([httpLink]);
}

function createApolloClient(ctx?: SchemaContext) {
  return new ApolloClient({
    name: "life-hive-dashboard",
    ssrMode: typeof window === "undefined",
    link: createIsomorphicLink(ctx || undefined),
    cache: new InMemoryCache({
      typePolicies: {
        Customer: { keyFields: ["customer_id"] },
        ApiaryDevice: { keyFields: ["device_id"] },
        HiveDevice: { keyFields: ["device_id"] },
        CombDevice: { keyFields: ["comb_id"] },
        Treatment: { keyFields: ["treatment_id", "device_id"] },
        DeviceEvent: { keyFields: ["event_id"] },
        CombState: { keyFields: ["id"] },
        DeviceState: { keyFields: ["id"] },
        Failure: { keyFields: ["failure_id"] },
        Query: {
          fields: {
            getGlobalIDs: relayStylePagination(),
          },
        },
      },
    }),
  });
}

interface InitApollo {
  initialState?: any;
  ctx?: SchemaContext;
}

export function initializeApollo({ ctx, initialState }: InitApollo) {
  const _apolloClient = apolloClient ?? createApolloClient(ctx || undefined);

  // If your page has Next.js data fetching methods that use Apollo Client, the initial state
  // gets hydrated here
  if (initialState) {
    // Get existing cache, loaded during client side data fetching
    const existingCache = _apolloClient.extract();

    // Merge the initialState from getStaticProps/getServerSideProps in the existing cache
    const data = merge(existingCache, initialState, {
      // combine arrays using object equality (like in sets)
      arrayMerge: (destinationArray, sourceArray) => [
        ...sourceArray,
        ...destinationArray.filter((d) =>
          sourceArray.every((s) => !isEqual(d, s)),
        ),
      ],
    });

    // Restore the cache with the merged data
    _apolloClient.cache.restore(data);
  }
  // For SSG and SSR always create a new Apollo Client
  if (typeof window === "undefined") return _apolloClient;
  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient;

  return _apolloClient;
}

export function addApolloState(
  client: ApolloClient<NormalizedCacheObject>,
  pageProps: { props: any },
) {
  pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract();

  return pageProps;
}

export function useApollo(pageProps: any) {
  const state = pageProps[APOLLO_STATE_PROP_NAME];
  const store = useMemo(
    () => initializeApollo({ initialState: state }),
    [state],
  );
  return store;
}
pages/_app.tsx
function MyApp({ Component, pageProps }: AppProps) {
  const router = useRouter();
  const apolloClient = useApollo(pageProps);
  return (
    <ApolloProvider client={apolloClient}>
          <Component {...pageProps} />
    </ApolloProvider>
  );
}

export default MyApp;

With the new Next13 AppDir they suggest creating a provider.tsx which is rendered client side and used to wrap the children in the layout.tsx as stated in their docs, but since i'm adding the apollo state to the app in my useApollo() this doesn't work.

So i've tried to make a simplier version by following some other blog posts on using Next and Apollo to initialize the client and then try useQuery() in a RSC:

config/apollo_config.ts
function createApolloClient() {
  return new ApolloClient({
    name: 'internal-dashboard',
    uri: process.env.GRAPHQL_URL_PROD,
    cache: new InMemoryCache(),
  });
}

export function useApollo() {
  const client = useMemo(() => createApolloClient(), []);
  return client;
}
app/providers.tsx
'use client';

import { ApolloProvider } from '@apollo/client';
import { useApollo } from '../config/apollo_client';

export default function Provider({ children }: { children: React.ReactNode }) {
  const client = useApollo();
  return <ApolloProvider client={client}>{children}</ApolloProvider>;
}

When running on a server rendered component i get the following error:

TypeError: Cannot read properties of undefined (reading 'Symbol(__APOLLO_CONTEXT__)')
    at Object.getApolloContext (webpack-internal:///(sc_server)/./node_modules/@apollo/client/react/context/context.cjs:22:49)
    at useApolloClient (webpack-internal:///(sc_server)/./node_modules/@apollo/client/react/hooks/hooks.cjs:27:46)
    at Object.useQuery (webpack-internal:///(sc_server)/./node_modules/@apollo/client/react/hooks/hooks.cjs:100:29)
    at useProductsQuery (webpack-internal:///(sc_server)/./graphql/generated/graphql-codegen.tsx:239:56)
    at ProductContent (webpack-internal:///(sc_server)/./app/(product)/ProductContent.tsx:12:125)
    at attemptResolveElement (webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1207:42)
    at resolveModelToJSON (webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1660:53)
    at Object.toJSON (webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1121:40)
    at stringify (<anonymous>)
    at processModelChunk (webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:172:36)
    at retryTask (webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1868:50)
    at performWork (webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1906:33)
    at eval (webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1297:40)
    at scheduleWork (webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:52:25)
    at pingTask (webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1296:29)
    at ping (webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1309:40)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

and if i run it on a client rendered component i get:

fetch is not defined
    at new ApolloError (index.js?2dcd:29:1)
    at eval (QueryManager.js?f2a8:609:1)
    at both (asyncMap.js?acdc:16:46)
    at eval (asyncMap.js?acdc:9:57)
    at new Promise (<anonymous>)
    at Object.then (asyncMap.js?acdc:9:1)
    at Object.eval [as next] (asyncMap.js?acdc:17:1)
    at notifySubscription (module.js?4392:132:1)
    at onNotify (module.js?4392:176:1)
    at SubscriptionObserver.next (module.js?4392:225:1)
    at eval (iteration.js?8787:4:50)
    at Array.forEach (<anonymous>)
    at iterateObserversSafely (iteration.js?8787:4:1)
    at Object.next (Concast.js?c3b3:25:43)
    at notifySubscription (module.js?4392:132:1)
    at onNotify (module.js?4392:176:1)
    at SubscriptionObserver.next (module.js?4392:225:1)
    at eval (parseAndCheckHttpResponse.js?5a22:123:1)

At this point i'm just kind walking around in the dark as i can't really wrap my head around what needs to be happening in order for the app to work with client rendered and server rendered components and where apollo client fits in to get my graphql data that i need.

Any suggestions or links to working examples for someone using Next JS 13 + AppDir + ApolloClient would be much appreciated!

Versions

  System:
    OS: Windows 10 10.0.22621
  Binaries:
    Node: 18.10.0 - D:\Program Files\nodejs\node.EXE
    Yarn: 1.22.19 - D:\Program Files\nodejs\yarn.CMD
    npm: 8.19.2 - D:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Spartan (44.22621.819.0), Chromium (108.0.1462.42)
  npmPackages:
    @apollo/client: ^3.7.2 => 3.7.2

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions