I pass from server to the client following data: query, variables and payload (response from graphql). Before rendering App I want to somehow put this data in the Relay store. How can it be done on the client?
Solved problems:
renderToString for run queriesrenderToString for render app with the already populated store. Tricky, but works.QueryRenderer which firstly lookup data in the store, before making a network request.Main Restriction:
relayEnv.getStore().getSource().toJSON(); produced too much redundant data. Passing query, variables and payload much lighter. Is there an easy way to populate Relay store from query, variables and payload?
I solved SSR entirely in the network layer by synchronously resolving the data on initial render. This way I don't need to alter the react-relay behavior by checking if the data is in the store nor populate the relay store manually.
if (initialState && initialState[cacheKey]) {
const payload = initialState[cacheKey];
delete initialState[cacheKey];
return payload;
}
initialState is an object that contains a mapping of query+ variables to graphql responses that the server fetched. The snippet above is running just before my fetch function to short-circuit network requests if we have the data already.
In my case it can look like this:
{"query:HomeQuery:{}":{"data":{"viewer":{"authenticated":false}}}}
To generate the cache key I use this:
function generateCacheKey(operation, variables = {}) {
invariant(operation, 'generateCacheKey: Expected to rececive a Relay operation');
return `${operation.query.operation}:${operation.name}:${JSON.stringify(variables)}`;
}
We brought up doing this via the request-caching pattern on https://github.com/facebook/relay/issues/1881 as well. I have a very basic example up in https://github.com/4Catalyzer/found-relay/blob/v0.3.0-alpha.11/examples/todomvc-modern-universal/src/fetcher.js. I might open-source these utilities and update the SSR/network guide.
@taion I had the push/shift solution first (keep it simple) but I found it to be somewhat unstable due to small differences in the server vs client rendering path (payloads would be matched to the wrong query). Not saying it wouldn't work for the majority of cases, but just to be aware that it may happen.
Your keys are probably better than mine, yeah.
Have you had any issues with not using an explicit stable stringify for the variables?
@taion I'm using built-in mechanism from Relay for obtaining cache keys.
On the server-side I'm using relay-runtime/QueryResponseCache for gathering requests with responses.
Then pass data from it to the HTML output.
And on the client side using stableJSONStringify from relay-runtime/lib/stableJSONStringify for obtaining cache key from client request and checking it in sync mode from cached data provided by the server.
Have you had any issues with not using an explicit stable stringify for the variables?
Not yet, but its a valid point :)
@nodkz Can you please explain or provide an example how you achived the following?
SSR works in such way:
with first call of renderToString for run queries
then awaiting network layer which completes all queries
and after that second call of renderToString for render app with the already populated store. Tricky, but works.
WIth regards
@keepitsimple better to look at on demo app
https://github.com/damassi/react-relay-network-modern-ssr-example
I've wrote a blog post how I make Relay work well with SSR
https://dev.to/sibelius/adding-server-side-rendering-to-a-relay-production-app-30oc
https://medium.com/@sibelius/adding-server-side-rendering-to-a-relay-production-app-8df64495aebf
Using my approach you don't need to do 2 passes on renderToString
we fetch all route queries using fetchQuery based on matchedRoutes, and we render all QueryRenderer with fetchPolicy store-and-network, so it will render directly from the store
We serialize Relay Store data using this:
const queryRecords = environment
.getStore()
.getSource()
.toJSON();
window.__RELAY_PAYLOADS__ = ${serialize(relayData, { isJSON: true })};
we have this helper to create environment per server side or cliente side:
let relayEnvironment = null;
export const initEnvironment = (records = {}) => {
const network = Network.create(cacheHandler);
const source = new RecordSource(records);
const store = new Store(source, {
// This property tells Relay to not immediately clear its cache when the user
// navigates around the app. Relay will hold onto the specified number of
// query results, allowing the user to return to recently visited pages
// and reusing cached data if its available/fresh.
gcReleaseBufferSize: 10,
});
// Make sure to create a new Relay environment for every server-side request so that data
// isn't shared between connections (which would be bad)
if (typeof window === 'undefined') {
return new Environment({
configName: 'server',
network,
store,
});
}
// reuse Relay environment on client-side
if (!relayEnvironment) {
relayEnvironment = new Environment({
configName: 'client',
network,
store,
});
}
return relayEnvironment;
};
On client side, we pass records like this:
const environment = initEnvironment(window.__RELAY_PAYLOADS__);
@sibelius what does cacheHandler() look like here?
also what exactly does serialize() do?
I wrote an article on this. Basically I do what @sibelius does, but then I re-create that initial query's response, manually and pass it to the child. That way, the child "doesnt do a lookup" when it boots.
Yep that's what I'm doing at the moment. This is how the nextjs example is implemented
Not entirely true actually.... https://github.com/zeit/next.js/blob/canary/examples/with-relay-modern/lib/withData.js#L35 my method alleviates the need to even return that queryProps. Massive savings on page side.
I do however remember reading somewhere, that this method is wrong too... Because it breaks data binding when store data changes..
Sorry i mean this one: https://github.com/zeit/next.js/tree/canary/examples/with-react-relay-network-modern.
Yeah, i think its good enough for what most people are using the lib for though :)
@maraisr does your method work for pagination and refetch containers? I've got regular queries working, but when i try to render a pagination container i'm getting the error:
Relay(_MyPaginationContainer_) tried to render a context that was not valid this means that Relay(_MyPaginationContainer_) was rendered outside of a query renderer.
Should I be wrapping the RelayEnvironmentProvider in a QueryRenderer?
oh wait it works. Just had to update to hooks and update babel and fix some errors. thanks again!!
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Most helpful comment
I've wrote a blog post how I make Relay work well with SSR
https://dev.to/sibelius/adding-server-side-rendering-to-a-relay-production-app-30oc
https://medium.com/@sibelius/adding-server-side-rendering-to-a-relay-production-app-8df64495aebf
Using my approach you don't need to do 2 passes on renderToString
we fetch all route queries using
fetchQuerybased on matchedRoutes, and we render all QueryRenderer with fetchPolicy store-and-network, so it will render directly from the storeWe serialize Relay Store data using this:
we have this helper to create environment per server side or cliente side:
On client side, we pass
recordslike this: