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

Q: Do we have to create client on every request? #392

Open
Sam-Kruglov opened this issue Nov 24, 2024 · 6 comments
Open

Q: Do we have to create client on every request? #392

Sam-Kruglov opened this issue Nov 24, 2024 · 6 comments

Comments

@Sam-Kruglov
Copy link

Sam-Kruglov commented Nov 24, 2024

The readme here says that for client and for server I have to provide a factory method like getClient() or makeClient() and call that right before using. In the official docs the client instance is reused.

I'm just trying to figure out how to update local cache. The factory methods for both client and server have new InMemoryCache() inside, so the cache is reset on every request.

  1. Can I put the cache into a constant at least?
  2. How can I sync server client's cache to the client client's cache?
    • Your example mentions how to use the hooks, apollo wrapper, and preloaded queries for cache warmup but this is not a recommended way by next js - it is recommended to have a "data access layer" instead. So, I currently have a "data" folder with use client and server import 'server-only' | 'use server' files with extension .ts, so it only deals with data, not UI. I assume with the wrapper, you do reuse the client's client via react context but then why do you ask for a factory method in the readme? And I'm not sure how to update the cache.
  3. I tried reusing server client instance and I made the mutation return the object so that cache override kicks in since the __typename and id would be the same. But right after mutation returns, readQuery returns undefined. Any idea why?

My usecase is simple:

  1. fetch a list of objects in server component
  2. mutate 1 object in server action
  3. display changes in UI to that 1 object in client component
@Sam-Kruglov
Copy link
Author

Observations: if I put cache into a constant outside of the function, then cache persists after deployment. If I leave it as is, then cache drops after deployment or if I refresh without cache via browser (shift+cmd+R instead of cmd+R)

@phryneas
Copy link
Member

phryneas commented Nov 25, 2024

Can I put the cache into a constant at least?

Gonna start this one with a screenshot from the Apollo Client docs on server side rendering:
image

I can't stress this enough: if you are doing SSR, you should never share a client or cache between multiple requests. You will end up mixing private data of multiple users. Apollo Client was not meant to handle the data of multiple users, and even if you use no-cache, things like query deduplication could still lead to data of multiple users being mixed up.

  1. How can I sync server client's cache to the client client's cache?

You use this library. The core point of this library is that requests made on the server during rendering (either made by PreloadQuery in RSC or by useSuspenseQuery in SSRed Client Components) are synched to the browser without you doing any additional work.

As for "create a data abstraction layer": Apollo Client is your data abstraction layer here. The same pattern you see with Apollo Client's PreloadQuery you will see with swr by Vercel (they recently shipped something similar, but tbh., I cant find it to link to right now). They mean that you shouldn't write fetch by hand in every component and handle auth everytime, but that's what your Apollo Client makeClient function is.

  1. I tried reusing server client instance and I made the mutation return the object so that cache override kicks in since the __typename and id would be the same. But right after mutation returns, readQuery returns undefined. Any idea why?

Is there any benefit from using a server action here? Why not just make the mutation directly from the browser? You have a client-side cache that you want to update, so involving a third party server seems like a lot of work.

Server actions are great if you render all your HTML in a RSC, but the moment you rely on your client-side normalized cache, there isn't a lot of use for them tbh.

This circles back to this in our README:
image

Always decide if some data only exists in RSC and the HTML rendered by RSC/Server Actions, or if it's always used by dynamic components based on a cache that lives in the client. Rendering the same data in both of these environments means that something on your screen will end up with stale data.


PS: don't worry, on the client you will have one long-existing client, it will not be recreated but accumulate data.

@Sam-Kruglov
Copy link
Author

Sam-Kruglov commented Nov 25, 2024

Sorry my brain broke.. Thanks a lot for helping out! Can you confirm if I got that right?

I have a bunch of UI to render a list of items and then each item is editable. So, I render them all in RSC, wrap each item in a form with server action, and stream via suspense.

  1. I see, "Apollo Client is your data abstraction layer", I suppose if I move GQL args to the UI code it's alright, I still have all the client config in my data folder, which is what matters.
  2. Now that I read the README much closer, even though I've read it multiple times before, I see that using <ApolloWrapper> actually doesn't make a component client, and neither does <PreloadQuery>. So, I can keep most of my setup as is, but I need to change the items component from RSC to client, add these 2 things. Then the items data will be transferred to the browser and assigned to the ApolloClient cache inside the wrapper, but then the browser has to CSR the items. Browser will still have the suspense boundary and see that the items are loading, just the time to when the items are fully rendered will be a bit later because we now have to wait for some computation. I suppose I'm fine with that.
    • It looks like a tradeoff, like, if I think the items are rarely edited, then I should be fine with refreshing the page on edit instead and go full SSR every time (i.e. keep the items component as RSC).
    • The example shows that I need to wrap my items not only in <Suspense> but also wrap <PreloadQuery> on top of that. Can I not put <PreloadQuery> inside of my items component or does it have to be outside the suspense boundary?
    • I've glanced over the suspense docs but still not sure, maybe you have a quick answer but don't bother too much:
      • why is there a need for useSuspenseQuery? I defined suspense boundary via <Suspense> and I can call client.query() normally, so it's the boundary that decides if it's suspended or not, why is there a specific hook for it? It just feels unnecessary and repetitive that I have to say "suspense" twice.
      • Why do we need the concept of queryRef if we can just rely on the background/preload query to populate the cache? Just try querying it again, it'll go to the cache first anyway, right?
  3. Using server action to call GQL mutation doesn't make sense here because there is no extra overhead on it that we'd want to move to the server. If it involved compute load or long operation, or something like that, then it would make sense. Am I correct and are there other usecases for server actions you'd normally use?
  4. Apollo wrapper calls the factory function once and stores the client in the react context, so that way it's reused. And since it's on the client, only that customer's data is cached, so there's no conflict.

On the server, we always call the factory function for the sole purpose of dropping the cache on each request so that there's no possibility to mix multiple customers in there.

  1. Why even have cache if it's only for 1 request? I learned about data loaders but that's a problem for the backend that hosts GQL API, right? In my case, I use apollo client on the backend to call graphql API served elsewhere, so I guess that doesn't apply and makes the cache useless.
  2. Why would the data be mixed for many customers? Can't it just check customer id or is it not configurable? I have the most experience implementing APIs than in frontend and it's usually never a problem.

@phryneas
Copy link
Member

phryneas commented Nov 26, 2024

I try to reply inline. I know, this is a lot to take in, and even after two years we're still figuring out best practices ourselves.

To preface everything, I want to give you one puzzle piece that might be important, though:

On first render, both RSC and Client Components render on the server. This library has three distinct jobs:

  • allow you to make requests in RSC (registerApolloClient().query)
  • transport data fetched in RSC into your Client Component cache, both the SSR CC cache and the Browser CC cache (PreloadQuery)
  • transport additional queries that might happen during the SSR pass of Client components from the server into the browser so they don't repeat there (useSuspenseQuery/useBackgroundQuery in combination with the Wrapper)

Sorry my brain broke.. Thanks a lot for helping out! Can you confirm if I got that right?

I have a bunch of UI to render a list of items and then each item is editable. So, I render them all in RSC, wrap each item in a form with server action, and stream via suspense.

1. I see, "Apollo Client is your data abstraction layer", I suppose if I move GQL args to the UI code it's alright, I still have all the client config in my data folder, which is what matters.

2. Now that I read the README much closer, even though I've read it multiple times before, I see that using `<ApolloWrapper>` actually doesn't make a component client, and neither does `<PreloadQuery>`. So, I can keep most of my setup as is, but I need to change the items component from RSC to client, add these 2 things. 

ApolloWrapper is what creates your ApolloClient instance for your client components and passes it into ApolloProvider, alongside setting up a ton of extra logic needed for data transport between the different layers.

Then the items data will be transferred to the browser and assigned to the ApolloClient cache inside the wrapper, but then the browser has to CSR the items. Browser will still have the suspense boundary and see that the items are loading, just the time to when the items are fully rendered will be a bit later because we now have to wait for some computation. I suppose I'm fine with that.

As I said, your Client Components already render on the server for the initial page load, so that first render happens on the server, not on the client. I don't think there's a perceivable difference in speed of first render between doing things in RSC or CC.

   * It looks like a tradeoff, like, if I think the items are rarely edited, then I should be fine with refreshing the page on edit instead and go full SSR every time (i.e. keep the items component as RSC).

Keep in mind that for subsequent renders it is probably faster to do things in a Client Component, since then you don't have the back-and-forth between client and server.
What RSC give you is the ability to use dependencies in your components that don't need to be shipped to the browser. They don't necessarily speed anything up.

   * The example shows that I need to wrap my items not only in `<Suspense>` but also wrap `<PreloadQuery>` on top of that. Can I not put `<PreloadQuery>` inside of my items component or does it have to be outside the suspense boundary?

I believe you can put PreloadQuery pretty much everywhere, but in an optimal world you'd have one accumulated GraphQL query per route (using fragment colocation), which is why we put that at the top in the examples

   * I've glanced over the [suspense docs](https://www.apollographql.com/docs/react/data/suspense) but still not sure, maybe you have a quick answer but don't bother too much:
     
     * why is there a need for `useSuspenseQuery`? I defined suspense boundary via `<Suspense>` and I can call `client.query()` normally, so it's the boundary that decides if it's suspended or not, why is there a specific hook for it? It just feels unnecessary and repetitive that I have to say "suspense" twice.

The <Suspense component defines where a loader is shown if any child waits for data to be loaded. useSuspenseQuery is the hook that actually starts the loading process and informs the suspense boundary that something is loading.

     * Why do we need the concept of `queryRef` if we can just rely on the background/preload query to populate the cache? Just try querying it again, it'll go to the cache first anyway, right?

You can skip the queryRef, and combine PreloadQuery with useSuspenseQuery, but we have found it very helpful to carry something around that gives you confidence via TS which data actually has been preloaded.

3. Using server action to call GQL mutation doesn't make sense here because there is no extra overhead on it that we'd want to move to the server. If it involved compute load or long operation, or something like that, then it would make sense.  Am I correct and are there other usecases for server actions you'd normally use?

Personally I haven't found a lot of good uses for server actions in applications that actually have a lot of dynamic data - and that's the user base we work with most. They are great if you e.g. don't want to have any data-fetching-related JS in the browser bundle and have mostly static data.

4. Apollo wrapper calls the factory function once and stores the client in the react context, so that way it's reused. And since it's on the client, only that customer's data is cached, so there's no conflict.

On the server, we always call the factory function for the sole purpose of dropping the cache on each request so that there's no possibility to mix multiple customers in there.

5. Why even have cache if it's only for 1 request? I learned about data loaders but that's a problem for the backend that hosts GQL API, right? In my case, I use apollo client on the backend to call graphql API served elsewhere, so I guess that doesn't apply and makes the cache useless.

You can still do things like making one big query on the page level and then your child components just read from the cache instead of making additional requests.

6. Why would the data be mixed for many customers? Can't it just check customer id or is it not configurable? I have the most experience implementing APIs than in frontend and it's usually never a problem.

In the cache, it's one big blob of normalized data with no inidication what the originating query looks like or which headers you sent along with it. So if a query can be resolved from the cache, there's no indiciation if it was actually fetched for this user. You could maybe get around that with very specific schema design, but that would probably not make for a good schema. And even that would be difficult once multiple users are allowed to see the same entitity, but have different access levels, as the same entity will always merge together.

@Sam-Kruglov
Copy link
Author

Sam-Kruglov commented Nov 26, 2024

Makes sense, except 1 thing. You're saying there's no difference if I put items component into RSC or CC because they are both rendered on the server the first time the page is loaded. But I'm using <Suspense> which is supposed to defer that component, so in my understanding, it should work in both cases because I do see loading spinner in both cases.

I tested both in RSC devtools. It only works if I navigate from one page to another, not on page refresh. So I went to home and then clicked start recording, then hit the link to the items page.

  • RSC and await getClient().query(..)
    • Loaded ~31kb uncompressed via RSC:
      • list of items already rendered as HTML except each item is a component with props <li item={{..query data for the item..}}>.
    • No graphql requests in the browser.
    • Apollo cache is empty in browser unless <PreloadQuery> is used.
  • CC and useSuspenseQuery:
    • Loaded 18kb uncompressed via RCS:
      • I see the items component as .js file loaded in.
    • No graphql requests in the browser.
    • Apollo cache is automatically populated, I see a <script> tag in the HTML with window[Symbol.for("ApolloSSRDataTransport")] ??= []).push(..query data..) - I tried with throttled network, I didn't see the data in the returned HTML even when the loading suspense state was shown, so the suspense works and waits for the data.

So, it appears that there is a difference - CC will do CSR and RSC will do SSR. And both will make the request on the server.

I think for my usecase CSR is better because after editing 1 item, I may need to sort all items again based on a simple criteria and going to server is a waste. Also the items component is very simple and doesn't use any heavy libs, so no need to over-optimize it. Plus, I don't need to bother with <PreloadQuery> because client cache is updated automatically that way.

I think I'm good for now, thanks for the help! Feel free to correct me on anything or otherwise close this issue.

@phryneas
Copy link
Member

So, it appears that there is a difference - CC will do CSR and RSC will do SSR. And both will make the request on the server.

CC will also do SSR on first page load. But it is React's "out of order streaming SSR", so the HTML will be out of order and assembled by JS that is also sent down the wire inline.
See these slides (sorry, German, ignore the text ^^) from page 31.

You can block loading of .js files in your devtools and will see the full page render in from the server.

If you were a search engine, in both cases you wouldn't get spinners but the full rendered HTML in order - the difference is that after that, you also stream down the JS for Client Components and they start hydrating and become interactive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants