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

What will Apollo code look like in server components? #20

Open MidnightDesign opened 1 year ago

MidnightDesign commented 1 year ago

(Sorry for using an issue to ask a question.)

From my understanding, this package so far only tells me how to instantiate an Apollo Client in Next.js 13. (I'm sure it's doing way more, but on a surface level, that's the only thing I could see.)

But I'm confused about how I'm supposed to use Apollo on the server. Here's my thought process:

To avoid too many GQL requests on the server, my first instinct would be to have one GraphQL request somewhere at the root of a page and pass the data down to other server components using props - kind of like we used to do it using getStaticProps. But that doesn't seem very ergonomic to me. This also leads to data being fetched unnecessarily when you change or remove some child components and nothing tells you to remove the unused fields from your query.

Another option I can think of is using fragments in the individual components and Apollo somehow combining them into a single query for you. I don't know if that can work, but it seems like the most intuitive option to me. But I think this approach might be at odds with Next.js's fetch-level caching.

The last approach that comes to my mind involves having many individual queries, with each component fetching its own data. I guess this would be the most cacheable approach, but it could result in dozens or even hundreds of requests per server-rendered page.

However, with the first and last approaches, I struggle to see the benefits of using the Apollo Client.

Can someone explain to me the vision of how we are supposed to use Apollo Client on the server?

phryneas commented 1 year ago

The honest answer is: we'll have to figure that out ourselves, still.

The first part was making it possible at all - that's what this package does. Patterns will need to emerge.

Right now, I would imagine that you probably use fragment composition to build one big query with all the fragments that your components need, and client.query that on page level. Then you would use client.readFragment in components further down the tree to access this data. You can find a (non-RSC) example with a lot of fragment composition here: https://github.com/apollographql/spotify-showcase As for the Next.js's fetch-level caching: you can disable that by using fetchOptions: { cache: "no-store" }, as a constructor option for HttpLink, and that might or might not be a good idea, depending on how dynamic the data you want to render is.

Generally, yes, a normalized Client is more useful if you have a long-running process and are not only rendering a snapshot, but I think there is still a lot of value to be had here.

But I'd also say that if you have more dynamic data, you'll probably want to explore using suspense in Client Components (those will also server-render on first page load!) in combination with streaming SSR.

MidnightDesign commented 1 year ago

Thank you for your comprehensive response. Feel free to close this or leave it open (for now) as a foundation for further discussion.

phryneas commented 1 year ago

I'll leave it open for now - I'm sure this will come up a lot, and more people might want to chime in :)

switz commented 12 months ago

One pattern I'd like to figure out is running querys from RSC that pass down data to client components who then run the same fragment? (query) as a subscription and take over realtime data on the client. I use hasura so all of my graphql queries have subsequent subscriptions with the same exact data.

The advantage here is I can actually render my entire app on the server, but once the client loads in, a subscription starts that keeps real time data up to date.

I'm not quite sure how to solve this yet, on more important pages I've been manually merging the data via fetch and then a subscription on the client. It works, but a more native/smoother solution would be really nice.

If anyone has any ideas, I'm happy to be a sounding board.

phryneas commented 12 months ago

@switz we've been discussing this here over in the RFC discussion.
TLDR: Prefetching like you describe here is something we don't support yet, but will look into as one of the next steps. Thank you for bringing up another valid use case for this!

IGassmann commented 12 months ago

I've been asking myself the same question for the past two months. I wonder how useful it is to use the cache on the server. Since the cache is request scoped, I suppose it results in almost no query hitting it because all queries are starting in parallel. Is that right?

Maybe there can be a per-field deduplication mechanism that checks if a field is already being requested before starting a query.

I'm linking this related conversation for reference: https://twitter.com/i_gassmann/status/1643578192693788672

Mad-Kat commented 12 months ago

What interests me is the streaming story. Right now the RSC and Suspense patterns pushes you in a direction where everything should be fetched at the beginning and once it's done the data is pushed to the client components.

As far as I know Streaming with @defer doesn't work right now and the only solution is to either eagerly fetch it or fetching the data multiple times (server and client).

What are your thoughts on that matter?

phryneas commented 11 months ago

@IGassmann it's the same behavior as Apollo Client on the client side - if those two queries would actually be the same query, they would be deduplicated. But we assume that if you request the same data twice with exactly the same fields, you'll probably be using the same query for that, so two different queries with the same field would still run as far as I know.

Generally we would recommend you to use fragment composition and query further up the tree to avoid that, with useSuspenseQuery or the newly upcoming useBackgroundQuery (although we are still working on the App dir story for that one).

phryneas commented 11 months ago

@Mad-Kat that's also one for useBackgroundQuery and useReadQuery (we're just adding those to the core Apollo Client), which would allow you to start a query in a parent component and then wait for that deferred data to come in in suspending child components. That said, at some point you might just want to cut off deferred queries on the server when they take too long - and unfortunately, defer doesn't allow to resume a query started somewhere else, so in that case there isn't too much way around restarting the whole query.

Mad-Kat commented 11 months ago

@phryneas Thank you for the clarification. This means if I follow the recommended way with fragment composition, @defer is basically out of the equation (for now 😄) and it would be better and easier to use different queries for deferred data that have a different loading strategy.

phryneas commented 11 months ago

@Mad-Kat it pretty much depends on what your deferred data looks like - if these are just a few fields that usually come back fast, but can sometimes hold a query back, it might make sense to wait for a maximum timeout on the server, and retry on the client in a slow case. If these are queries that are known to take long under all circumstances, it might make sense to separate them out and just useQuery for them only in the browser.

IGassmann commented 11 months ago

@IGassmann it's the same behavior as Apollo Client on the client side - if those two queries would actually be the same query, they would be deduplicated. But we assume that if you request the same data twice with exactly the same fields, you'll probably be using the same query for that, so two different queries with the same field would still run as far as I know.

@phryneas, yeah, I was thinking more about the case of different queries but with some fields that are the same.

Considering we would still have "field duplication" across different queries when using the Apollo client in server components, what are the advantages of using it over a more lightweight GraphQL client such as graphql-request (and the React cache() function if we need to deduplicate queries that are the same)?

Generally we would recommend you to use fragment composition and query further up the tree to avoid that, with useSuspenseQuery or the newly upcoming useBackgroundQuery (although we are still working on the App dir story for that one).

So far, we've been using fragment composition with graphql-request + GraphQl Codegen's client-preset when querying in server components. However, the ergonomics aren't great since you need to (1) think of a name for your fragment, (2) add it to a parent component's query, and (3) pass it down and unmask it. You have more code and also more code that needs to be renamed when refactoring stuff.

phryneas commented 11 months ago

@IGassmann in the long run, I could imagine that we add a multi-layered cache that differentiates between unauthenticated fields that will share a cache between all requests, and authenticated fields that are queried for every request - so when we get there, there will definitely be value in ApolloClient inside of RSC.

So far, we've been using fragment composition with graphql-request + GraphQl Codegen's client-preset when querying in server components. However, the ergonomics aren't great since you need to (1) think of a name for your fragment, (2) add it to a parent component's query, and (3) pass it down and unmask it. You have more code and also more code that needs to be renamed when refactoring stuff.

You might want to give useSuspenseQuery and useFragment a go for that. We have a demo here that makes heavy use of fragment composition: https://github.com/apollographql/spotify-showcase. That would probably save you a few corners there..

nick4fake commented 10 months ago

I am sorry, just to clarify: is fetching on server side supported now? We've tried multiple options, but it dosn't seem to work. Is there an example?

Basically:

Attempted to call useQuery() from the server but useQuery is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.

With this example:

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

export const { getClient } = registerApolloClient(() => {
  return new ApolloClient({
    cache: new InMemoryCache(),
    link: new HttpLink({
      // https://studio.apollographql.com/public/spacex-l4uc6p/
      uri: "https://main--spacex-l4uc6p.apollographos.net/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" },
    }),
  });
});

It tries to make request on compile time

Edit: Apparently Next.js ignores fetch cache on apollo client. We've forced it to mark route as dynamic:

export const dynamic = 'force-dynamic';
phryneas commented 10 months ago

@nick4fake we have a section in the README on fetching in RSCs, and a part of our polls demo fetches in RSC. You don't use the useQuery hook, but getClient().query.