apollo-client-nextjs
apollo-client-nextjs copied to clipboard
Fetching data on RSC and hydrate the client with that data
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
orrefetch
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
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.
(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}
@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.
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 🙌🏻
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?
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.
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 😉.
Any updates on this?
@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,
})
// ...
}
@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?
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!
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 :)
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.