Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
lloydrichards opened this issue Dec 9, 2022 · 35 comments
Closed

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

lloydrichards opened this issue Dec 9, 2022 · 35 comments

Comments

@lloydrichards
Copy link

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
@bignimbus
Copy link
Contributor

Hi @lloydrichards, thanks for posting this Issue! The team is really interested in RSC and, as you note, there are some changes that will need to be made in order to make Apollo Client and Next.js 13 work together out of the box. We're working on supporting React 18 features like Suspense and renderToPipeableStream for the 3.8 release (see #10231) and the first alpha should ship soon. After learning from those implementations we'll be able to move with more confidence toward an understanding of how to support developers who are using Server Components. If anyone finds this thread and wants to share tips, please do!

@lloydrichards
Copy link
Author

glad to hear things will be moving in the future, and will just have to wait on using ApolloClient for the time being. I did some experimenting and managed to get things working first with fetch() and then with graphql-request. part of debugging was creating a minimal repo for it, which is now fixed and might be useful for others:

https://github.com/lloydrichards/reproduced-fetch-error

but most of the credit for getting working goes to https://github.com/graphqlwtf I'll keep my eyes open for any future development in ApolloClient and wish you luck!

@jpvajda
Copy link
Contributor

jpvajda commented Dec 12, 2022

@lloydrichards thanks for letting us know about the challenges here. You can follow along with some upcoming Alpha releases we have planned to better support React 18 Suspense and SSR (see our Roadmap for more details) Hopefully once this is better supported you can give Apollo Client a try again.

@partounian
Copy link

partounian commented Dec 13, 2022

hey all,

I'm using NextJs 13.0.6, and Apollo Client 3.7.2. The issue could be specific to using a credentials (email/password) provider with Nextauth.js, but it seems like whenever I try to perform an apollo query on the server side, I get an error. "[Network error]: TypeError: fetch failed".

The same code seems work fine on the client side.

Any chance y'all have a guess as whether this is a bug/incompability between these libraries or if I'm doing something wrong? Not sure if the nextjs override for fetch() could be the cause of this issue

If it's any help, I am mostly following https://github.com/shadcn/taxonomy which is the only solid Nextjs 13 example I'm found.

@partounian
Copy link

partounian commented Dec 13, 2022

Maybe I'm doing something wrong, but seems like this fixed my issue #10310 (comment)
(using cross-fetch instead of fetch for @apollo/client http links)

@msikma
Copy link

msikma commented Dec 20, 2022

I'm curious if there's any information yet about what the recommended interface will look like for using Apollo with Next 13.

For example, this blog post for Next 12 talks about using the old getStaticProps() and getServerSideProps() api that is not supported in the /app dir.

I realize this is all very new and I probably should not be using the /app dir yet if I want to use Apollo, but some thoughts on this would be appreciated since as far as I can tell there's no real documentation on this yet.

@lloydrichards
Copy link
Author

lloydrichards commented Dec 20, 2022 via email

@antizhyk
Copy link

Hi all , has anyone been able to use NextJS 13 and Apollo Client yet? I'm starting a new project and really want to do it on NextJS 13, but I need tools to work with GraphQL.

@lloydrichards
Copy link
Author

lloydrichards commented Jan 12, 2023 via email

@antizhyk
Copy link

antizhyk commented Jan 12, 2023

But then NextJS will work like regular React, right? And for my project SEO is very important.

you can still use Apollo Client, but you'll end up with everything in the app directory being a client component. If that's okay with you then all you need to do is wrap children in the layout.tsx in the ApolloProvider, the same way you would in _app.tsx. I think with the new Typescript plug it it should automatically detect that everything is a client component so you don't need to use use client but you'll have to avoid any of the getServerSideProps patterns that you would use in Pages.

@adamfortuna
Copy link

adamfortuna commented Jan 12, 2023

Hi all , has anyone been able to use NextJS 13 and Apollo Client yet? I'm starting a new project and really want to do it on NextJS 13, but I need tools to work with GraphQL.

It is possible to make calls from the server side. I've been using the same setup for app as previously in pages for the server-side part. Here's an example from a project I'm working on.

I've found this way works for hitting a GraphQL API directly, but it doesn't do server-side caching, so it makes requests each time (I think). In my case I need to make this call in head.tsx and in page.tsx, so the call gets made twice. 😅 If anyone knows how to cache this call, I'd welcome feedback. :)

@/app/[slug]/page.tsx

import { getPostBySlug } from '@/queries/wordpress/getPostBySlug'
import { ArticleContentHtml } from '@/components/articles/ArticleContent'
import { ArticleHeader } from '@/components/articles/ArticleHeader'

interface PageProps {
  params: any
  children?: React.ReactNode
}
export const revalidate = 60 * 60 // 60 minutes
export default async function Page({ params }: PageProps) {
  const article = await getPostBySlug(params.slug)
  if (!article) return null

  return (
    <>
      <ArticleHeader article={article} />
      <ArticleContentHtml article={article} />
    </>
  )
}

@/queries/wordpress/getPostBySlug'

import wordpressClient, { parsePost } from '@/lib/wordpressClient'
import { gql } from '@apollo/client'

export const findWordpressPost = gql`
query GetWordPressPost($slug: String!) {
  post: postBy(slug: $slug) {
    title
  }
}
`

export const getPostBySlug = (slug: string) => {
  return wordpressClient
    .query({
      query: findWordpressPost,
      variables: {
        slug,
      },
    })
    .then((result) => {
      if (!result.data.post) {
        return null
      }
      return parsePost(result.data.post)
    })
}

@/lib/wordpressClient

import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'

const clientHttpLink = createHttpLink({
  uri: 'https://wp.adamfortuna.com/graphql',
})
const clientAuthLink = setContext((_, { headers }) => {
  return {
    headers: {
      ...headers,
      authorization: `Basic ${process.env.WP_ADAMFORTUNA_TOKEN}`,
    },
  }
})
const client = new ApolloClient({
  link: clientAuthLink.concat(clientHttpLink),
  cache: new InMemoryCache(),
})
export default client

This is pretty basic, but it allows for using GraphQL with Apollo, with an authenticated API. I'm still working on having Next.js 13 cache the network call from getPostBySlug though. If anyone has any tips on that I'd love to hear them.

@baxterw3b
Copy link

@adamfortuna Hey Adams, that's what i'm doing too, but for me the revalidate doesn't work after build, does it for you? For the cache, did you try with the defaultOptions? :

const client = new ApolloClient({
  uri: `http://localhost:3000/api/graphql`,
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: "cache-and-network",
      errorPolicy: "none",
    },
    query: {
      fetchPolicy: "network-only",
      errorPolicy: "all",
    },
  },
});

@karlhorky
Copy link

karlhorky commented Jan 19, 2023

the revalidate doesn't work after build

@baxterw3b What expected behavior are you referring to that is not working? There are some interesting gotchas with revalidate in the current Next.js 13 app/ directory, probably bugs that will be fixed.

@mehdy-benha
Copy link

Any new advances on this? Still getting: TypeError: Cannot read properties of undefined (reading 'Symbol(__APOLLO_CONTEXT__)')" unfortunately. Temporarily moved to grapwhl-request like @lloydrichards.

@baxterw3b
Copy link

baxterw3b commented Feb 7, 2023

the revalidate doesn't work after build

@baxterw3b What expected behavior are you referring to that is not working? There are some interesting gotchas with revalidate in the current Next.js 13 app/ directory, probably bugs that will be fixed.

I would expect that if i set a revalidate of 10 seconds after those 10 seconds if i refresh the page that data would be new, instead i had the cached data.

@karlhorky
Copy link

karlhorky commented Feb 9, 2023

Looking at this more, I guess the Next.js app/ directory and React Server Components are a bit at odds with the current version of Apollo Client. The usage of context via <ApolloProvider> does not work across Server Component <> Client Component boundaries (and ServerContext is not really something to pass large values in as mentioned in this thread with @sebmarkbage)

@jerelmiller @bignimbus Maybe a new API for Apollo Client + React Server Components is is needed, especially to enable scenarios such as:

  1. Run a server-side GraphQL query in an Server Component + reuse the server-side GraphQL cache from that query in a client component

This scenario is described further in Handling Apollo Client Cache Hydration in Next.js 13's app directory #10466

Other libraries using context also have this issue, eg. Emotion, where there will also be an investigation of providing a special API for Server Components.

@jetaggart
Copy link

@lloydrichards out of curiosity, are you using graphql-request with next's new fetch method, with tanstack/query or soemthing else?

@gena-tokarev
Copy link

gena-tokarev commented Feb 22, 2023

Hey, everyone, I found a solution (workaround).

1. First off we need to extend Apollo client and add some additional functionality to it. You may not do it exactly this way. The main idea is to save query promises when you call each query: (in my approach they are stored in the activeQueries property)

import {
    ApolloClient as BaseApolloClient,
    InMemoryCache,
} from "@apollo/client";
import { ApolloClientOptions } from "@apollo/client/core/ApolloClient";

let global = {};

try {
    global = window;
} catch {}

class ApolloClient<TCacheShape> extends BaseApolloClient<TCacheShape> {
    constructor(options: ApolloClientOptions<TCacheShape>) {
        super(options);
    }

    public activeQueries: ReturnType<ApolloClient<TCacheShape>["query"]>[] = [];

    query: BaseApolloClient<TCacheShape>["query"] = (options) => {
        const promise = super.query(options);
        this.activeQueries.push(promise);
        return promise;
    };
}

export const apolloClient = new ApolloClient({
    ssrMode: true,
    uri: "http://localhost:4000/graphql",
    cache: new InMemoryCache().restore(global.__APOLLO_STATE__),
    connectToDevTools: true,
});

2. Then create such a component:
The code for script was taken from the official docs (https://www.apollographql.com/docs/react/performance/server-side-rendering/#executing-queries-with-getdatafromtree)

import React from "react";
import { apolloClient } from "@/apolloClient";

const ApolloExtractCache = async () => {
    await Promise.all(apolloClient.activeQueries);

    return (
        <script
            dangerouslySetInnerHTML={{
                __html: `window.__APOLLO_STATE__=${JSON.stringify(
                    apolloClient.extract()
                ).replace(/</g, "\\u003c")};`,
            }}
        />
    );
};

export default ApolloExtractCache;

3. Lastly, render this component at the bottom of your tree in the main layout.
When all the promises get resolved it'll extract the cache and save the result to a global var which will be used on the client for initializing its value:

import "./globals.css";
import ApolloProviderWrapper from "@/ApolloProviderWrapper";
import ApolloExtractCache from "@/ApolloExtractCache";

export default function RootLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    return (
        <html lang="en">
            {/*
        <head /> will contain the components returned by the nearest parent
        head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
      */}
            <head />
            <body>
                <ApolloProviderWrapper>
                    {children}
                    <ApolloExtractCache />
                </ApolloProviderWrapper>
            </body>
        </html>
    );
}

Please note that this is only a demo. It extends only the query method. If you need mutation as well, you can do pretty much the same following this pattern.

Side note:
In your server components you should call queries this way:
await apolloClient.query(...)
Because hooks useQuery, useMutation won't work there.

@stephyswe
Copy link

This is such a hack.
Hope a more stable way is developed soon : )

@pistachiomatt
Copy link

@Gasheck's solution is very clever but has anyone got the ApolloExtractCache part to successfully extract the cache? I'm always getting an {}

@gena-tokarev
Copy link

gena-tokarev commented Mar 13, 2023

@Gasheck's solution is very clever but has anyone got the ApolloExtractCache part to successfully extract the cache? I'm always getting an {}

@pistachiomatt Where exactly do you get an empty object? In your browser console?
Try to log console.log(apolloClient.extract()) somewhere in ApolloExtractCache and look at your console where next.js is running. If it's empty there too, then you probably just have an empty apollo cache. You just don't request any data from your graphql server.
You need to have at least one component in the component tree that fetches graphql data.

Also make sure those components which fetch data use apolloClient.query method. I mentioned that it's just a demo of a possible workaround and it only works with apolloClient.query, so if you want to use it with apolloClient.mutation or other methods, you have to extend these methods as well following the same pattern or improve the solution in your desired way.

@everest113
Copy link

Apollo has released some guidance https://www.apollographql.com/blog/apollo-client/how-to-use-apollo-client-with-next-js-13/

@karlhorky
Copy link

@patrick91 thanks for this blog post! Does this share the cache between Server Components and Client Components? Seems like it does not... 🤔 So maybe it does not have a lot of the optimizations discussed in this thread here yet...

@patrick91
Copy link

@karlhorky you're right, the blog post is mostly about using RSC with Apollo.

My understanding is that since Server Components are only executed in the server, sharing the Apollo Cache might not be that useful, as the server component won't re-render when the cache changes.

I'm looking into a follow-up blog post on how to share the cache, I'll probably based it on @Gasheck findings (thanks for that), but as I said above, the Server Components' code won't be sent, so we'd need to find another solution there. I know the vercel team is working on an RFC for mutations, maybe there's something there for us 😊

We are also experimenting with making some utilities for Next.js to reduce the code you have to write to make Apollo Client work properly there, but I'm not sure when that will be ready 😊

@mengqing
Copy link

Does anyone know what would be the ideal pattern to use fetchMore for pagination with RSC?

@gfpacheco
Copy link

Does anyone know what would be the ideal pattern to use fetchMore for pagination with RSC?

I'm trying to build this as we speak, this is the direction I'm going:

  • Make the component that has pagination a Client Component and query normally using hooks
  • Use await getDataFromTree to make sure the server has fetched all queries
  • Let Next13 render the tree normally (now that Apollo has cached all queries)
  • Send the initial state to the client for rehydration

@phryneas
Copy link
Member

phryneas commented Apr 5, 2023

For the problem that has been mentioned in the first post here,

TypeError: Cannot read properties of undefined (reading 'Symbol(APOLLO_CONTEXT)')

You can use the experiential build from over in #10726:

npm i @apollo/client@0.0.0-pr-10726-20230405113624

More experimentation around this and a RFC will likely drop within the next few weeks.

@robigan
Copy link

robigan commented Apr 20, 2023

Hi all , has anyone been able to use NextJS 13 and Apollo Client yet? I'm starting a new project and really want to do it on NextJS 13, but I need tools to work with GraphQL.

It is possible to make calls from the server side. I've been using the same setup for app as previously in pages for the server-side part. Here's an example from a project I'm working on.

I've found this way works for hitting a GraphQL API directly, but it doesn't do server-side caching, so it makes requests each time (I think). In my case I need to make this call in head.tsx and in page.tsx, so the call gets made twice. 😅 If anyone knows how to cache this call, I'd welcome feedback. :)

@/app/[slug]/page.tsx

import { getPostBySlug } from '@/queries/wordpress/getPostBySlug'
import { ArticleContentHtml } from '@/components/articles/ArticleContent'
import { ArticleHeader } from '@/components/articles/ArticleHeader'

interface PageProps {
  params: any
  children?: React.ReactNode
}
export const revalidate = 60 * 60 // 60 minutes
export default async function Page({ params }: PageProps) {
  const article = await getPostBySlug(params.slug)
  if (!article) return null

  return (
    <>
      <ArticleHeader article={article} />
      <ArticleContentHtml article={article} />
    </>
  )
}

@/queries/wordpress/getPostBySlug'

import wordpressClient, { parsePost } from '@/lib/wordpressClient'
import { gql } from '@apollo/client'

export const findWordpressPost = gql`
query GetWordPressPost($slug: String!) {
  post: postBy(slug: $slug) {
    title
  }
}
`

export const getPostBySlug = (slug: string) => {
  return wordpressClient
    .query({
      query: findWordpressPost,
      variables: {
        slug,
      },
    })
    .then((result) => {
      if (!result.data.post) {
        return null
      }
      return parsePost(result.data.post)
    })
}

@/lib/wordpressClient

import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'

const clientHttpLink = createHttpLink({
  uri: 'https://wp.adamfortuna.com/graphql',
})
const clientAuthLink = setContext((_, { headers }) => {
  return {
    headers: {
      ...headers,
      authorization: `Basic ${process.env.WP_ADAMFORTUNA_TOKEN}`,
    },
  }
})
const client = new ApolloClient({
  link: clientAuthLink.concat(clientHttpLink),
  cache: new InMemoryCache(),
})
export default client

This is pretty basic, but it allows for using GraphQL with Apollo, with an authenticated API. I'm still working on having Next.js 13 cache the network call from getPostBySlug though. If anyone has any tips on that I'd love to hear them.

I would like to improve on this implementation by integrating with standard JS practices a bit better. My difference is that I have created my own query "hook" that can be passed to React's use hook to automatically use Suspense. The query hook is similar to getPostBySlug but universally accepts all queries and mimics Apollo's useQuery hook.
See my example in my repo, specifically https://github.com/robigan/movie-night/blob/main/app/(sidebar-home)/movie/%5Bid%5D/page.tsx and https://github.com/robigan/movie-night/blob/main/lib/graphql-client.ts (Privatized due to IB restrictions)

Downside to this approach that I've seen people say (haven't tested myself) is that revalidation doesn't work (There seems to be a large discussion about how to implement Streaming and revalidation on the RFC for React 18) and the cache isn't working?

In the meantime I'd say this is a good solution given that the beta for 3.8.0 is scheduled to release in 15d

@jerelmiller
Copy link
Member

jerelmiller commented Apr 21, 2023

@robigan thanks for sharing!

My difference is that I have created my own query "hook" that can be passed to React's use hook to automatically use Suspense

I presume that since you're using use here, this is a client component that runs in the browser. This solution works great for your initial request, but as soon as you need cache updates, you will have no way to receive them. For example, lets say you have a mutation that updates that votes field on the movie. Your Movie component wouldn't be able to show this update without refetching this data from the server. To do that, you're likely going to need some kind of "invalidation" strategy to tell your Movie component that it needs to refetch.

If you don't need to respond to those kinds of cache updates, then this is a totally reasonable solution! There just happens to be some additional complexity to consider here as well, which is what makes this so tricky. Food for thought!

FWIW this is something the upcoming useSuspenseQuery hook will handle for you so you can get the benefits of suspense, but allow your component to receive cache updates as well.

the cache isn't working?

It's not so much that the cache isn't working, its more that there is a lot of added complexity in React 18 and Suspense that we previously didn't have.

In React 17 and prior, rendering was synchronous. This meant rendering your app in SSR was done in a single pass. Apollo's SSR approach took advantage of this by waiting for all your queries to settle before the HTML was generated and sent to the browser. Because we could wait for all queries to settle, this meant we could serialize the entire cache and send it to the browser along with the rest of your app's HTML. This allowed the browser to hydrate the Apollo cache using the data fetched on the server. This client cache hydration was necessary to prevent your client components from fetching again, since your server already did the work.

In React 18, there is no longer a synchronous rendering model. React now has the superpower to stream HTML updates from the server to the client incrementally as parts of your app are ready to be displayed. React determines these "boundaries" by using Suspense. Because parts of your app might now be sent to the browser while other parts are still executing on the server, we can no longer wait for all queries to settle before we can send the cache results to the browser.

And this is the problem we are trying to solve now. We need to be able to incrementally stream those cache updates from the server to the browser to prevent the client from re-executing those queries. This allows us to avoid refetching those queries on the client when React hydrates those components. Ideally a lot of this complexity is hidden away from you.

That was a ton of info, but hopefully that helps you understand where we are at! We will have a public RFC that goes over this a lot more in detail in the coming week or two. We'll be sure to post an update in the original RFC (and likely here) if you're interested in learning more.

@hari4698
Copy link

hari4698 commented May 1, 2023

glad to hear things will be moving in the future, and will just have to wait on using ApolloClient for the time being. I did some experimenting and managed to get things working first with fetch() and then with graphql-request.

Hey, @lloydrichards. Were you able to use graphql-request for client components? It is working for server components for me. It would be great if you can provide an example of the client components in case you were able to figure it out.

@robigan
Copy link

robigan commented May 1, 2023

@robigan thanks for sharing!

My difference is that I have created my own query "hook" that can be passed to React's use hook to automatically use Suspense

I presume that since you're using use here, this is a client component that runs in the browser.

Just want to clarify for future reference that use is server based and should be server based fwik. All use does is accept a promise and wait for it to finish, triggering suspense while the promise resolves.

@lloydrichards
Copy link
Author

glad to hear things will be moving in the future, and will just have to wait on using ApolloClient for the time being. I did some experimenting and managed to get things working first with fetch() and then with graphql-request.

Hey, @lloydrichards. Were you able to use graphql-request for client components? It is working for server components for me. It would be great if you can provide an example of the client components in case you were able to figure it out.

the dashboard i was working on ended up only getting the data from the server components and then passed things down to my charts and interactive elements that were client side. I've been waiting for some more clarity on RSC in general as Next has a very opinionated way of using them which i'm not sure how other libraries will follow if they support other frameworks using different patterns 🤷

@hari4698
Copy link

hari4698 commented May 1, 2023

the dashboard i was working on ended up only getting the data from the server components and then passed things down to my charts and interactive elements that were client side. I've been waiting for some more clarity on RSC in general as Next has a very opinionated way of using them which I'm not sure how other libraries will follow if they support other frameworks using different patterns 🤷

I'm also building a dashboard with interactive elements like filters. I thought I had to get data in the same client components. Do you happen to have a sample in a public repo that I can use as a reference?

@phryneas
Copy link
Member

phryneas commented May 11, 2023

Hey everyone, I just wanted to let you all know that we released a package to support React Server Components and the new streaming SSR for Client Components that the Next.js App router offers.

You can find the package repository over here: @apollo/experimental-nextjs-app-support
Here is an introductory blog post: Using Apollo Client with Next.js 13: releasing an official library to support the App Router
And if you want to dive deep in the tech, and why we made certain limitations, there is a very long RFC with a discussion going on in RFC: The Next.js "App Router", React Server Component & "SSR with Suspense" story

I will be closing this issue here and would love it if you could give us feedback and suggestions for improvements over in the other repo!

@github-actions
Copy link
Contributor

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
For general questions, we recommend using StackOverflow or our discord server.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 21, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests