-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
How do we do server-side rendering with Relay Modern? #1881
Comments
To get by for now, I'm using this code: https://github.com/taion/relay-todomvc/blob/found-modern-universal/src/fetcher.js Essentially a one-time use response cache, that uses the order of the requests (which for these purposes will be deterministic) as the cache keys, and expunges records from the cache as they're consumed. Is this the right sort of approach? |
Yeah, I'm interested to know how to do it properly. Right now I'm sort of following this but in a bit different way. Basically I'm making request to graphql server using I'm not even sure if I'm using relay properly with those cache system, refetch and pagination later on. I'm also not using |
Did you found a good way to solve the SSR problem with Modern Relay ? Would be awesome when facebook would be also interested in SSR for Relay and could do some documentation on how to solve it. |
I think something like what I have for my Relay-TodoMVC example is basically correct. It just needs to be hardened to deal with errors and such. Just checking if there's any feedback on anything I'm missing, though. |
So let me get this straight. You're querying the GQL endpoint on the server, dumping the responses into an array that you serialize to the client, and then when the client loads and goes to execute a GQ, first it dumps all payloads into the Relay Cache on the client and then continues to try a normal "fetch" which wouldn't actually need to hit the API if the payloads from the server give all the data needed for the query on the client... Right? |
You should follow the pattern in https://github.com/4Catalyzer/found-relay/blob/master/examples/todomvc-modern-universal/src/fetcher.js for now. Note the caching is at the level of the network requests, not the Relay data. |
Or check https://github.com/este/este |
@taion I really like the pattern there and I am asking these questions to assist in my attempt to follow the pattern of your fetcher. The ServerFetcher builds an array of payloads that gets returned to the server.js file: which prints the payloads to the page for the client-side JavaScript: The client-side JS initializes a ClientFetcher with the payload and the API URL and then immediately creates a resolver with the fetcher: So my question is how are the payloads here being accounted for: https://github.com/4Catalyzer/found-relay/blob/master/examples/todomvc-modern-universal/src/fetcher.js#L52 ? Is there a loop somewhere that calls fetch until the payload list is empty, and then continues with other queries on the page? How would I do this with React Router 4? https://github.com/4Catalyzer/found-relay/blob/master/examples/todomvc-modern-universal/src/client.js#L21 is assuming I'm using a farce router -- the main change that is necessary is to account for the resolver in RR4. Any advice in this regard? |
The I would recommend not using RRv4 with Relay if you're using routes with nested requests, as it's not certain the same guarantees will apply, and you'll potentially have issues with request waterfalls. See https://facebook.github.io/relay/docs/routing.html#nested-routes for more details. |
At the moment I'm not experiencing request waterfalls, as my server renders one giant request per route. Request waterfalls are something I will watch out for as I continue to build out the SPA, and if I encounter such a situation I will switch to using found. Thank you so much, @taion, as I now understand how this works. I might cheat a little bit and add something to the ClientFetcher container which just "fetches" all the payloads when init'd. From there, all subsequent queries should hit the cache and get what they need without any concern for ordering. Yes? |
oh, and what do you mean by, "Note the caching is at the level of the network requests, not the Relay data." ? |
As in the fetcher handles the caching... so really it's your network layer that's handling it, and it only knows about requests, not about the schema per se. |
Ahh I see, so the same request generates the same response, and it's the request->resposne pair that's cached as opposed to some form of data tree with merges / diffs. Did you ever have any luck with the QueryRenderer @taion ? Currently, I have one request per route; however, this only triggers on the server. The front-end I plan to use FragmentContainers on, which should trigger appropriate queries as they render in? Will I still need a QueryRenderer somewhere? I've had a heck of a time getting the QueryRenderer to work with SSR... |
I'm reading #1687, #1760, and https://github.com/robrichard/relay-query-lookup-renderer right now to try and get caught up with everything discussed so far. |
I was able to get server rendering working by using slightly modified versions of the query-lookup-renderer and getDataFromTree from react-apollo. Basically I added a hook to the queryrenderer to get a promise for its query, and walk the render tree waiting for each queryrenderer to finish before continuing. This saves the data in the relay store instead of at the network layer, and has worked pretty well for me. @taion How do you solve the initial render, as the network layer is asynchronous? |
Okay I did manage to get this working to an extent using the QueryLookupRenderer -- but I end up stuck with a stagnant cache after page load. I have it temporarily triggering page reloads when the data changes, and alternatively plan to not use the cache at all; however, a long-term solution would be ideal. Note that I went with the QueryLookupRenderer only because my current codebase is in React Router 4. I believe found-relay to be my eventual goal, and with it perhaps the simpler version of the network caching from above will be sufficient. But one thing at a time. |
@chadfurman Make sure the QueryRenderer only looks up from the cache on initial load. If you're having trouble with stagnant cache after mutations, I believe there's a bug in
in the QueryRenderer constructor. |
@johanobergman my problem was that my QueryRenderer was using an |
@taion i think one of the main things about SSR is SEO. Sending relay payload to the client and then hydrate there works for a user but not a crawler. right? What is bad about doing this? |
Hydration is to get the site working again on the client. For pure SSR, it’s just the markup you care about. You hydrate to pick things up on the client, not for the initial render. |
Here's an example utilizing Next.js which should be pretty easy to follow: https://github.com/zeit/next.js/tree/canary/examples/with-relay-modern |
That's not a very good implementation... you're not caching the response results at all, so in fact you block on the client-side render until the initial fetch succeeds. Hydrating the store in this method: https://github.com/zeit/next.js/blob/canary/examples/with-relay-modern/lib/createRelayEnvironment.js#L30 is basically a no-op, as I noted in the OP. In other words, as far as I know, you still need something like https://github.com/4Catalyzer/found-relay/blob/v0.3.0-alpha.9/examples/todomvc-modern-universal/src/fetcher.js to cache the responses, unless you want to block interaction on the client-side until that data fetch succeeds, which would be a pointless delay to interactivity. |
My main concern with fetcher.js is XSS -- I'm not sure how to get the server's payloads rendered to the client. JSON.stringify alone is not enough, as you can see in the redux docs:
Maybe I'll use yahoo serialize? https://medium.com/node-security/the-most-common-xss-vulnerability-in-react-js-applications-2bdffbcc1fa0 |
Yes – that's exactly what I do in https://github.com/4Catalyzer/found-relay/blob/v0.3.0-alpha.9/examples/todomvc-modern-universal/src/server.js#L79 (though The point of this issue is that, ATM, the only options are to either do this bit with the requests, or to use the lookup query renderer. The former is a bit annoying, but the latter has some edge cases since you're bypassing the normal Relay request flow. |
also you can do: <script src="path/to/bundle.json" data-data="${escape(JSON.stringify(preloadedState))}"></script>then in your bundle use |
I didn't mean window.escape, just some html escape function |
@petesaia that’s an interesting approach, but adds a 200ms race to each SSR request. My approach uses renderToString to trigger relay requests. When all requests are finished our relay network handler triggers another renderToString. If this causes more relay requests, the cycle repeats. We clamp this at 3 iterations right now, but don’t have a situation that requires more than 2. I’ve been curious about the performance between my strategy and one the uses jsdom, but haven’t had time to test. I’ll post a gist up when I get a chance. Our reasons are SEO and initial page load; while we employ several cache strategies (CDN, service worker, app cache, etc) our app bundle is quite large and takes a while to download and parse. This makes for a really frustrating first interaction, especially on spotty mobile networks, which is an important target for us. Additionally, time to first meaningful render is used as a factor when ranking search results for mobile. |
Thanks @mike-marcacci! Yeah, @taion, I'm starting to consider going that route. With the request caching is there some worry with the request order, not sure how deterministic it is? |
When setting up a server i think that this issue, and other issues such as serving multiple apps, authenticated react apps, relay integration and so on should be handled by a complete server package. And you should be able to edit it all after your needs. I have done this here: https://github.com/velop-io/server @taion would you like to look into it hand take part as a collaborator? |
@grydstedt The order in which requests get sent out is deterministic, so the request caching approach is fine as long as the requests are the same. It's the same as how it worked with isomorphic-relay-router. @Stoffern I don't think there's much specific to the server here. For example, something like https://github.com/4Catalyzer/found-relay/blob/v0.3.0-alpha.11/examples/todomvc-modern-universal/src/server.js just looks like a standard React "server rendering" example. |
Guys, you may test a beta version of my SSR middleware for API will be changed a little bit, but this is working solution which I use in my app. |
@nodkz - awesome! gonna take this for a spin right now |
I have been using this Create React App + Relay + GraphQL.js + SSR template for all of my consultancy projects during the last few years, and it works super well. Check it out: https://github.com/kriasoft/react-firebase-starter (✮ 4k) A couple of notable things that it does:
Basically, declare your routes somewhat similar to this (they can have any shape/form you like): const routes = [
{
path: '/products/:orderBy(featured|newest)?',
query: graphql`
query ProductsQuery($first: number, $orderBy: ProductsOrederBy) {
products(first: $first, orderBy: $orderBy) {
...ProductList_products
}
}
`,
variables: ctx => ({ first: 50, orderBy: ctx.params.orderBy }),
components: () => [import(...), import(...)], // .js chunks for the route
render(components, data, ctx) {
return {
title: '...',
body: <ProductList productsRef={data.products} />,
};
}
},
...
]; Tell the router what needs to happen when a route is matched to the provided URL ( import UniversalRouter from 'universal-router';
import { createOperationDescriptor, getRequest, fetchQuery } from 'relay-runtime';
import routes from './routes';
export default new UniversalRouter(routes, {
resolveRoute(ctx) {
const { route, params, relay } = ctx;
...
let dataPromise = null;
let componentsPromises = ....;
if (route.query) {
const request = getRequest(route.query);
const operation = createOperationDescriptor(request, variables);
const lookup = relay.lookup(operation.fragment, operation);
const response = fetchQuery(relay, route.query, variables);
dataPromise = lookup.isMissingData ? response : lookup.data;
}
return Promise.all([...componentsPromsies, dataPromise]).then(...);
}
} Then bootstrap the router when application starts ( import { createBrowserHistory } from 'history';
import router from './router';
function render(location) {
router.resolve(location).then(...).catch(...);
}
const history = createBrowserHistory();
history.listen(render);
render(history.location);
`` |
Has anyone been able to use a refetch container or pagination container with https://github.com/zeit/next.js/tree/canary/examples/with-relay-modern. I can't work out how to do anything other than a basic query. |
Unfortunately I am unable to use
I hope that someone had more success and is willing to share the experience! |
can you try to use the community hooks version https://github.com/relay-tools/relay-hooks? |
This seems likely to be specific to the setup with Next. Are you properly setting up the query renderer context? |
can you try the new hooks version b83aace? |
After playing around I was able to get it working (non-hooks version) in Next.js with the following technique: On the server I do:
This would provide props that are not
And the
I get the following logs:
Changing
|
You can try something like this, rendering
|
@robrichard this works like a charm! |
pretty sure, that the problems here are solved with this PR #2883 |
@wasd171 any chance you could share a working example? |
@wasd171 how did you get this not to blow out of control over time, or is the source, store and environment re-created per request?
|
@robrichard do you have a wrapper component that handle this behavior different on server vs on client? |
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 anybody wanna take a stab to add this to the official docs? |
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. |
Here is yet another example of how to hydrate/dehydrate Relay store at CDN edge locations using Cloudflare Workers. kriasoft/graphql-starter-kit#277 See |
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. |
This is sort of a follow-up to #1687 and #1760.
What is a recommended pattern for handling server-side rendering in a simple request-response paradigm with Relay Modern?
RecordStore
seems to expose capabilities for serializing and deserializing its contents, and I can use a preloadedRecordStore
when building a newEnvironment
, but this is a bit deceptive, since without #1760, my<QueryRenderer>
s will ignore the preloaded records... but this capability must be there for a reason?But thinking about this more, I'm not even sure the approach in #1760 is correct. For a streaming network layer, it seems like it would be wrong to omit sending out the request just because I have preloaded data – instead I'd just want the server to e.g. not send me the initial payload (because I already have it).
Something like the query response cache seems more promising, but the bundled one doesn't really expose hooks for exporting its contents, plus a key/value cache with a TTL isn't exactly the right model here anyway.
Am I missing something?
The text was updated successfully, but these errors were encountered: