relay
relay copied to clipboard
Support for React Server Components
Today you can't use Relay with React Server Components, because the RelayEnvironment is handed around using React Context.
Most of our components will still remain client-only, but there are some of our pages that would benefit greatly from being statically pre-rendered using React Server Components. In these components we'd also like to use Relay's useFragment
to be able to re-use existing fragments and developer knowledge.
Since the Relay Environment context is basically just a dependency injection mechanism, it could also be replaced by React's cache
API on the server. Maybe we could define that useRelayEnvironment
consumes the environment from a React cache instead of context in server-components or on the server in general. This could be achieved through the react-server
conditional export.
Are there any objections to this or other ideas? Otherwise I'll try to create a proof-of-concept over the coming weeks.
It would be great to have a useFragment that just works everywhere. I've gotten fragments to work on the server w/ a server-specific useFragment and prop-drilling the environment, but having different fragment call signatures can mean having server and client versions of what should be the same components.
The proposed solution using the package.json
export would not alter the signature of useFragment or useRelayEnvironment. Just a different implementation would be used whether the code is executed in Server Components or on the client.
check this https://github.com/relayjs/relay-examples/pull/270
Do you have ideas on how to handle usePaginationFragment?
Great question @tobias-tengler , and thank you for bringing this up!
Yes, your suggestion of building a different version of APIs designed to run on the server is correct - and that's what I did for our internal prototype for RSC and Relay. However, we don't have the same control over what modules to load using conditional flags in node.js, so we just built a different module for Relay on the server. Nothing fancy there - just a lookup a fragment in the store, and fetchQuery.
For the React's cache
- yeah, you could do that, but you can also just use globals since cache is needed for kind of expensive computations you want to reuse. But for the Relay environment, we just added a module where you need to call a method to create an environment before you run any RSC rendering.
Basically, the server module of Relay will have 3 things: useFragment, loadQuery, and something to create/register an environment.
As for pagination and refetch - these have to be driven by your RSC framework (I don't think there is a good way now to share the state of the store between client and the server, so usePaginationFragment - a client-only hook won't be able to really work on the server).
An interesting aspect is the integration of fetchQuery
and RSC streaming - in my prototype, I just added support for a single request-response format (where fetchQuery returns a promise), but it is possible to make fetchQuery with observables to stream RSC payloads (I think).
Anyways, it is a very interesting area to experiment with, so feel free to look into that and build a POC, and I'm happy to provide reviews (when available).
Thank you again for looking into this!
I think usePagination and useRefetch can do the initial fetch on the server, and the refetch queries on the client
The problem with refetching, is that it also need to re-render server components that use data from these hooks, so you need to referch the RSC not the graphql endpoint
Could it be feasible to break up usePaginationFragment, where the initial fetch is done on the server, and the refetch is imported into a Server Action?
I've started some work on a library that enables Relay in RSCs. https://github.com/Pokeyo-AB/relay-rsc
Looks like there's some movement to get server components back from a server action: https://sdk.vercel.ai/docs/ai-sdk-rsc/streaming-user-interfaces
Addendum: It turns out that server actions can return components, even suspense wrapped components. Pretty neat! However, if you're using Next, you'll probably want to familiarize yourself with this issue and workaround: https://github.com/vercel/next.js/issues/58125
I am also working on creating a PoC.
- https://github.com/facebook/relay/issues/4107#issuecomment-2067997601
- https://github.com/mizdra/poc-nextjs-app-router-relay
Here are the key points:
- Use
readInlineData
instead ofuseFragment
so that data can be read by both the Client Component and the Server Component.- https://github.com/mizdra/poc-nextjs-app-router-relay/blob/40e4e501dbd9c6bd047715988730c6df48d93486/components/User.tsx#L12
- Do not import
react-relay
packages from the Server Component. Instead import therelay-runtime
package.- This is because the react-relay package uses an API (
createContext
) specific to the Client Component. - Import
graphql
andreadInlineData
functions from therelay-runtime
package, not thereact-relay
package - https://github.com/mizdra/poc-nextjs-app-router-relay/blob/40e4e501dbd9c6bd047715988730c6df48d93486/components/User.tsx#L4
- This is because the react-relay package uses an API (
- To make
RelayEnvironmentProvider
a Client Component, use a wrapper component with the'use client'
directive- https://github.com/mizdra/poc-nextjs-app-router-relay/blob/40e4e501dbd9c6bd047715988730c6df48d93486/components/RelayEnvironmentProvider.tsx
- Use
fetchQuery
on the Server Components to fetch queries- https://github.com/mizdra/poc-nextjs-app-router-relay/blob/main/lib/relay/fetchGraphQLQuery.ts#L16
- https://github.com/mizdra/poc-nextjs-app-router-relay/blob/40e4e501dbd9c6bd047715988730c6df48d93486/app/article/%5BarticleId%5D/page.tsx#L19
- https://github.com/mizdra/poc-nextjs-app-router-relay/blob/40e4e501dbd9c6bd047715988730c6df48d93486/app/layout.tsx#L33
- Publish the data fetched on the Server Component to the Relay Store so that ClientComponent can read it.
- https://github.com/mizdra/poc-nextjs-app-router-relay/blob/40e4e501dbd9c6bd047715988730c6df48d93486/app/layout.tsx#L38
- https://github.com/mizdra/poc-nextjs-app-router-relay/blob/40e4e501dbd9c6bd047715988730c6df48d93486/app/article/%5BarticleId%5D/page.tsx#L37
- https://github.com/mizdra/poc-nextjs-app-router-relay/blob/40e4e501dbd9c6bd047715988730c6df48d93486/components/RelayRecordMapPublisher.tsx#L33
- Retain data fetched on the Server Component so that it is not deleted from the Store by garbage collection
- https://github.com/mizdra/poc-nextjs-app-router-relay/blob/40e4e501dbd9c6bd047715988730c6df48d93486/components/RelayRecordMapPublisher.tsx#L38
- If you want to use pagination, call
usePaginationFragment
in the Client Component- https://github.com/mizdra/poc-nextjs-app-router-relay/blob/40e4e501dbd9c6bd047715988730c6df48d93486/app/article/%5BarticleId%5D/CommentsCard.tsx#L12
Unresolved issues:
- Re-rendering by mutation
- Relay Store is updated when mutation is executed
- Normally, when the Relay Store is updated, the component that is reading the data in the
useFragment
is re-rendered. - However, this PoC uses
readInlineData
for many parts. Therefore, there is a problem that components usingreadInlineData
are not re-rendered. - Perhaps the component you want to re-render after performing mutation should be a Client Component and use
useFragment
. - Or you may need to re-render the Server Component using something like
router.refresh()
in Next.js.
- Executing mutations on Server Actions
- You can probably use
commitMutation
to execute mutations from Server Actions. - However, you will need to do some work to re-render the component after executing the mutation.
- To re-render the Server Component, you will need to call
revalidatePath
orrevalidateTag
. - It is difficult to re-render the Client Component, which reads data from the Relay Store using
useFragment
orusePaginationFragment
.- The results of the mutation need to be returned from the Server Actions and stored in the Relay Store
- However, the API for doing this is not exposed from
react-relay
andrelay-runtime
. - I don't think that's possible at the moment.
- To avoid this problem, do not execute mutations on Server Actions
- You should execute mutations on the client using
useMutation
.
- You should execute mutations on the client using
- To re-render the Server Component, you will need to call
- You can probably use
- Support for
@defer
and@stream
- Components that depend on
@defer
fields should be suspended and rendered streaming. - However, PoC does not currently implement that feature.
- Perhaps the module (
lib/relay/environment.ts
,lib/relay/fetchGraphQLQuery.ts
) that fetches the query needs to be modified - I also think we need to change readInlineData to return
Promise
.- The
Promise
is resolved when the data in the@defer
or@stream
field arrives. - Perhaps we should use a
use
hook to get the value out of thePromise
. This API can be used from either the Server Component or the Client Component.
- The
- Components that depend on
Feedback on relay maintainers:
- It would be useful if the react-relay package could also be imported from the Server Component
- It would be good to use
react-server
conditional exports to separate the artifacts for the Client Component from those for the Server Component.
- It would be good to use