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

Fetching data on RSC and hydrate the client with that data #124

Closed
nickbling opened this issue Nov 6, 2023 · 12 comments
Closed

Fetching data on RSC and hydrate the client with that data #124

nickbling opened this issue Nov 6, 2023 · 12 comments

Comments

@nickbling
Copy link

nickbling commented Nov 6, 2023

Hello,

I'm building a page that displays music albums, has an infinite loader and also a bunch of filtering options. I'm using Next.js v13.5.4 with the app dir, and Contentful to retrieve my data. In order to setup Apollo, I followed the guidelines reported here.

What I'd like to do is:

  • Make a first query on the RSC to fetch data
  • Pass that data to a client component in order to hydrate an Apollo Client that won't make the query another time, until fetchMore or refetch are called (for the infinite loader and the filters).

At the moment I'm handling the process like this:

// page.tsx --> RSC

const MusicCollection: FC = async () => {
  await getClient().query<AlbumCollectionQuery, AlbumCollectionQueryVariables>({
    query: ALBUM_COLLECTION_QUERY,
    variables: initialAlbumListFilters,
  })

  return <AlbumCollection />
}

export default MusicCollection
// AlbumCollection.tsx --> Client Component

'use client'

import { useSuspenseQuery } from '@apollo/experimental-nextjs-app-support/ssr'

export const AlbumCollection: FC = async () => {
  const { data, fetchMore, refetch } = useSuspenseQuery<
    AlbumCollectionQuery,
    AlbumCollectionQueryVariables
  >(ALBUM_COLLECTION_QUERY, {
    fetchPolicy: 'cache-first',
    variables: initialAlbumListFilters,
  })

// ...
}

My idea was that if I made the query on the RSC first, the client would have populated itself without making the query another time, but that's not happening and I don't think I understood how this should properly work. Since there are not many topics about it around, could you help me understand the correct workflow to follow for these use cases?

Thank you

@phryneas
Copy link
Member

phryneas commented Nov 6, 2023

The caches in Server Components and in Client Components are completely independent from each other - nothing from the RSC cache will ever be transported to the CC cache. (As for a general rule, Server Components should never display entities that are also displayed in Client Components).

You could try to do something like

// RSC file
export async function PrefetchQuery({query, variables, children}) {
  const { data } = await getClient().query({ query, variables })

  return <HydrateQuery query={query} variables={variables} data={data}>{children}</HydrateQuery>

}

// CC file
"use client";
export function HydrateQuery({query, variables, data, children}) {
  const hydrated = useRef(false)
  const client = useApolloClient()
  if (!hydrated.current) {
    hydrated.current = true;
    client.writeQuery({ query, variables, data })
  }
  return children
}

and then use it like

<PrefetchQuery query={ALBUM_COLLECTION_QUERY} variables={initialAlbumListFilters}>
  <AlbumCollection/>
</PrefetchQuery>

to transport the result over, but to be honest, I haven't experimented around with this too much yet.

@pondorasti
Copy link

pondorasti commented Nov 12, 2023

(As for a general rule, Server Components should never display entities that are also displayed in Client Components).

I see your reasoning for creating a separation between RSCs and CCs from a technical perspective. However, thinking about this from the end user, this statement is inherently saying that you can either use Apollo for highly static (RSCs) or dynamic (CCs) cases. However, most apps nowadays sit somewhere in the middle where they are aiming for dynamic at the speed of static experiences. In order to achieve this, you would need to make your RSCs and CCs work together.

Using getClient inside an RSC to kickoff a request as soon as possible, and then hydrating a CC for adding interactivity seems like something that should work directly in Apollo.

<Suspense fallback={<Skeleton />}>
  <PrefetchQuery query={...}>
    <ClientComponent />
  </PrefetchQuery>
</Suspense>

If you were to rewrite the code above by removing PrefetchQuery and only use useSuspenseQuery, it would lead to a slower experience in almost all cases. (since the network request gets waterfalled and triggered from the browser).

Putting that aside, I was running into the same issue when implementing infinite scrolling. Initially tried a cursed pattern by recursively returning RSCs, but kept running into problems with streaming or nextjs. Ended up implementing a solution similar to the one above. The only caveat, is that not all data is serializable and I need to use JSON.stringify/parse when passing data between the RCS and CC.

Warning: Only plain objects can be passed to Client Components from Server Components. Location objects are not supported.
  {kind: ..., definitions: [...], loc: Location}

@phryneas
Copy link
Member

@pondorasti our statements do not collide.

I said: "Server Components should never display entities that are also displayed in Client Components"

As long as you don't display those contents in your server components, but just somehow forward them to the cache of your client components, you are fine.

But if you render them out in a server component, and also render them out in a client component, and then the cache changes (as it does, because it is a dynamic normalized cache), your client components would all rerender with the new contents and your server components would stay as they are, because they are static HTML. You want to avoid that at all costs.

As for your problem at hand: we've been using the superjson library for that kind of serialization, and so far it worked well.

@pondorasti
Copy link

I said: "Server Components should never display entities that are also displayed in Client Components"

As long as you don't display those contents in your server components, but just somehow forward them to the cache of > your client components, you are fine.

Thanks for the detailed explanation and my apologies for the misunderstanding. This makes perfect sense. So what's the story for hydrating CCs from RSCs. Is something like a <PrefetchQuery /> component planning to be built at the framework level?

As for your problem at hand: we've been using the superjson library for that kind of serialization, and so far it worked well.

Great choice, could even pass those pesky date objects around and still work as expected. Thanks for the tip 🙌🏻

@phryneas
Copy link
Member

phryneas commented Nov 14, 2023

Thanks for the detailed explanation and my apologies for the misunderstanding. This makes perfect sense. So what's the story for hydrating CCs from RSCs. Is something like a component planning to be built at the framework level?

I've been thinking about it since we released the library, and I'm not 100% certain on what exactly the API should look like in the end (e.g. a more sophisticated variant might not need to wrap the children) - also it seemed that something on the React/Next.js side might be moving a bit, so I've held back more there, too (it seems like we could use taint to maybe ensure these values never get rendered in RSC? 🤔 ).

So yeah.. it's on my mind, and it will come eventually, but I'm also very busy with other parts of Apollo Client right now (including trying to get native support for all this into React so we won't need framework-specific wrapper packages anymore), so I can't guarantee for a timeline.

@pondorasti
Copy link

Awesome, thanks for sharing all these info with me. Looking forward to the changes, and will definitely keep an eye out for the RFC once you've figured out 😉.

@martingrzzler
Copy link

Any updates on this?

@martingrzzler
Copy link

@phryneas
react-query has something like this implemented. <HydrationBoundary/> What do you think of their approach?

// app/posts/page.jsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import Posts from './posts'

export default async function PostsPage() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return (
    // Neat! Serialization is now as easy as passing props.
    // HydrationBoundary is a Client Component, so hydration will happen there.
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts />
    </HydrationBoundary>
  )
}

and then the posts are immediately available on the client

// app/posts/posts.jsx
'use client'

export default function Posts() {
  // This useQuery could just as well happen in some deeper
  // child to <Posts>, data will be available immediately either way
  const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })

  // This query was not prefetched on the server and will not start
  // fetching until on the client, both patterns are fine to mix.
  const { data: commentsData } = useQuery({
    queryKey: ['posts-comments'],
    queryFn: getComments,
  })

  // ...
}

@wuzikai18
Copy link

@phryneas react-query has something like this implemented. <HydrationBoundary/> What do you think of their approach?

// app/posts/page.jsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import Posts from './posts'

export default async function PostsPage() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return (
    // Neat! Serialization is now as easy as passing props.
    // HydrationBoundary is a Client Component, so hydration will happen there.
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts />
    </HydrationBoundary>
  )
}

and then the posts are immediately available on the client

// app/posts/posts.jsx
'use client'

export default function Posts() {
  // This useQuery could just as well happen in some deeper
  // child to <Posts>, data will be available immediately either way
  const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })

  // This query was not prefetched on the server and will not start
  // fetching until on the client, both patterns are fine to mix.
  const { data: commentsData } = useQuery({
    queryKey: ['posts-comments'],
    queryFn: getComments,
  })

  // ...
}

did you succeed?

@jerelmiller
Copy link
Member

Hey all 👋

Support for preloading in RSC and using that data to populate the cache in client components was released in 0.11.0. If I'm reading the issue right, I believe this is what you're looking for!

@phryneas
Copy link
Member

phryneas commented Jul 1, 2024

Yup, as Jerel said, this has been released - please try it out.
If you have any feedback on the feature, please open a new issue so it's easier to track.
I'm going to close this issue as we now provide the feature that was asked for here :)

@phryneas phryneas closed this as completed Jul 1, 2024
Copy link
Contributor

github-actions bot commented Jul 1, 2024

Do you have any feedback for the maintainers? Please tell us by taking a one-minute survey. Your responses will help us understand Apollo Client usage and allow us to serve you better.

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

6 participants