-
Notifications
You must be signed in to change notification settings - Fork 786
Reimplement getDataFromTree using ReactDOM.renderToStaticMarkup. #2533
Reimplement getDataFromTree using ReactDOM.renderToStaticMarkup. #2533
Conversation
The |
@@ -23,6 +23,8 @@ export * from './withApollo'; | |||
|
|||
export * from './types'; | |||
|
|||
export * from './walkTree'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just avoid exporting this? Does anyone actually use walkTree
directly (not through getDataFromTree
), besides the tests in this repository?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have seen some people using it, but not many. I did a quick GH code search, and can find it being referenced in a couple of cases, but again not many. Maybe we consider dropping the export when we hit 3.0?
thing: state.thing + 1, | ||
userId: props.id, | ||
client: apolloClient, | ||
} as any)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we no longer control the way setState
updater functions are called, we can't rely on the context
being passed as a third parameter.
throw new Error('Should have thrown an error'); | ||
}, e => { | ||
expect(e.toString()).toEqual('Error: foo'); | ||
expect(e).toBe(fooError); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test was previously enforcing the functionality where we tried to trap multiple exceptions within their subtrees. Needless to say, this test is less useful now that only one exception will be thrown.
const markup = ReactDOM.renderToString(app); | ||
expect(markup).toEqual(html); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to use ReactDOM.renderToString
instead of renderToStaticMarkup
, you can. The benefit here is that you can just call getDataFromTree
once, without having to call renderToString
afterwards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This all looks really great @benjamn! I'm curious about the performance implications, but definitely agree with this future proofing approach. Thanks!
@@ -23,6 +23,8 @@ export * from './withApollo'; | |||
|
|||
export * from './types'; | |||
|
|||
export * from './walkTree'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have seen some people using it, but not many. I did a quick GH code search, and can find it being referenced in a couple of cases, but again not many. Maybe we consider dropping the export when we hit 3.0?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is amazing!! 😍I'm so happy you were able to get this working so quickly.
A couple questions:
Can we quantify the performance hit? It might be worth having Apollo SSR users with large apps (@staylor 👀) test this out so we know if it's viable in production.
Also, exceptions thrown during component rendering that are not related to data fetching can now abort the entire rendering, whereas previously we trapped errors within their specific subtrees.
This is consistent with how React handles errors that are uncaught by an error boundary, so I don't think it's a big deal. Do we know if this approach works with error boundaries? I'm assuming it does because rendering is performed by React, but it would be helpful to confirm so we can recommend it in the docs as a workaround.
For the const start = +new Date;
await getDataFromTree(WrappedApp);
const body = renderToString(WrappedApp);
console.log("server rendering took", new Date - start, "ms");
sink.renderIntoElementById('app', body); Here are the timings for ten requests using the previous tree-walking implementation of ~/dev/react-apollo/examples/ssr% meteor
[[[[[ ~/dev/react-apollo/examples/ssr ]]]]]
=> Started proxy.
=> Started MongoDB.
=> Started your app.
=> App running at: http://localhost:3000/
I20181026-14:17:07.631(-4)? server rendering took 146 ms
I20181026-14:17:13.668(-4)? server rendering took 23 ms
I20181026-14:17:14.432(-4)? server rendering took 20 ms
I20181026-14:17:15.321(-4)? server rendering took 21 ms
I20181026-14:17:16.261(-4)? server rendering took 18 ms
I20181026-14:17:17.130(-4)? server rendering took 29 ms
I20181026-14:17:18.036(-4)? server rendering took 23 ms
I20181026-14:17:18.982(-4)? server rendering took 17 ms
I20181026-14:17:19.902(-4)? server rendering took 17 ms
I20181026-14:17:20.807(-4)? server rendering took 17 ms Here are the timings for ten requests using the new implementation of I20181026-14:19:07.723(-4)? server rendering took 202 ms
I20181026-14:19:11.579(-4)? server rendering took 27 ms
I20181026-14:19:12.565(-4)? server rendering took 17 ms
I20181026-14:19:13.579(-4)? server rendering took 23 ms
I20181026-14:19:14.593(-4)? server rendering took 21 ms
I20181026-14:19:15.623(-4)? server rendering took 20 ms
I20181026-14:19:16.639(-4)? server rendering took 15 ms
I20181026-14:19:17.605(-4)? server rendering took 11 ms
I20181026-14:19:18.578(-4)? server rendering took 18 ms
I20181026-14:19:19.567(-4)? server rendering took 20 ms If I only use I20181026-14:19:52.365(-4)? server rendering took 80 ms
I20181026-14:19:53.485(-4)? server rendering took 15 ms
I20181026-14:19:54.436(-4)? server rendering took 16 ms
I20181026-14:19:55.293(-4)? server rendering took 17 ms
I20181026-14:19:56.162(-4)? server rendering took 19 ms
I20181026-14:19:57.004(-4)? server rendering took 26 ms
I20181026-14:19:57.790(-4)? server rendering took 15 ms
I20181026-14:19:58.636(-4)? server rendering took 15 ms
I20181026-14:19:59.481(-4)? server rendering took 20 ms
I20181026-14:20:00.210(-4)? server rendering took 15 ms In other words, there's no apparent performance regression for small component trees. Here's the entire server-rendered HTML string for this app (to quantify "small"): <div><div><div><h3>R2-D2</h3><h6>Luke Skywalker: newhope, empire, jedi</h6><h6>Han Solo: newhope, empire, jedi</h6><h6>Leia Organa: newhope, empire, jedi</h6></div></div></div> My hunch is that these rendering times are dominated by the |
@peggyrayzis Unfortunately, it sounds like error boundaries (specifically |
Data confirming that
Here's how I instrumented the code: if (!this.queryGraveyard.has(query, JSON.stringify(variables))) {
const start = Date.now();
this.queryPromises.set(
queryInstance,
new Promise(resolve => {
resolve(queryInstance.fetchData());
}).then(result => {
console.log(Date.now() - start, "ms to resolve", result);
return result;
})
);
... Note that every request gets a new |
This is great, I’m especially glad to hear about moving off a parallel implementation of walking the tree and instead relying on the public React API. However, I’m definitely concerned about the performance impact this would have, since renderToStaticMarkup() is synchronous, I expect a non-trivial impact on the performance for large apps (NYTimes). I can try and pull this branch down and run it against our app to see how it performs and post back, but in case it is severely impacted, do we have a plan B? |
If you have any specific test procedures/parameters you’d like me to use to benchmark, please share and we’d be happy to run them. Otherwise will try and run some Is this branch published/publishable anywhere to ease testing? |
@benjamn -- is it known when this last render is? If so, would it be possible to have this last render optionally do a |
This is a very solid strategy, and is almost identical to how I implemented our SSR for Relay (before we moved over to Apollo). It's a far less brittle solution than walking the tree, and is worth whatever performance overhead it may add. As I'm sure we all know, the react team is still plugging away on suspense and async rendering, which will eventually add first-class support for this kind of thing. In the meantime, I think the priority should be providing a way to handle SSR with Apollo which doesn't break on each new React release. Using |
@benjamn keep in mind that it isn't just rendering latency that is the problem; even if unnecessary CPU load (rendering strings) is only contributing a small percentage to the latency, it can seriously affect throughput by multiplying per-request CPU usage significantly. Having said that, I think it's the right choice! Plus I'm generally an advocate of not layering your GraphQL queries too deep anyway -- if you have only one or two queries per page this isn't going to be a big performance hit for you, right? |
12222d6
to
347b7a8
Compare
@tmeasday That's right! If you don't have any // This HTML should be renderable as-is:
const html = await getDataFromTree(root, renderToString);
// No need to call renderToString(root) here! |
In case anyone has been itching to try this out, I just published |
@tizmagik I see your point, but unfortunately I don't think it's possible to detect when the last rendering is going to happen, since it's now up to I think the benefit of getting your final HTML from |
@benjamn instead of rendering to string, would it make sense to try using react-reconciler to basically write a Admittedly, such an implementation would be more effort than this PR, but should hopefully be easier to maintain/support compared to the custom walker that's in (I'm still working on getting some real-world numbers from using this in our app and will post back) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also fixes what I managed to fix here: #2558
Thanks @benjamn -- agreed this is a good approach for now, and the fact that the renderer is configurable means one could implement a sort of I might look into doing just that, would you be open to a PR (for documenting the option, if anything)? |
@tizmagik I think I would like to see |
In [email protected], the rootContext parameter was replaced by the custom renderFunction parameter, which was a breaking change: #2533 (comment) This commit restores the previous API, while providing an alternative API that is more flexible and more accurately named: getMarkupFromTree. If you want to specify a custom rendering function, you should now use getMarkupFromTree instead of getDataFromTree: import { getMarkupFromTree } from "react-apollo"; import { renderToString } from "react-dom/server"; getMarkupFromTree({ tree: <App/>, // optional; defaults to {} context: { ... }, // optional; defaults to renderToStaticMarkup renderFunction: renderToString, }).then(markup => { // Use the markup returned by the final rendering... }); Hopefully, since [email protected] was never assigned the `latest` tag on npm, we can push this change without another minor version bump. cc @evenchange4 @hwillson
In [email protected], the rootContext parameter was replaced by the custom renderFunction parameter, which was a breaking change: #2533 (comment) This commit restores the previous API, while providing an alternative API that is more flexible and more accurately named: getMarkupFromTree. If you want to specify a custom rendering function, you should now use getMarkupFromTree instead of getDataFromTree: import { getMarkupFromTree } from "react-apollo"; import { renderToString } from "react-dom/server"; getMarkupFromTree({ tree: <App/>, // optional; defaults to {} context: { ... }, // optional; defaults to renderToStaticMarkup renderFunction: renderToString, }).then(markup => { // Use the markup returned by the final rendering... }); Hopefully, since [email protected] was never assigned the `latest` tag on npm, we can push this change without another minor version bump. cc @evenchange4 @hwillson
For anyone else having piggybacked on old getDataFromTree's fetchData behavior for non-apollo things(I was using it to await an async Redux thunk), it's because this PR and thus 2.3.0+ removed it. I know this was probably never intended to be used like that, but uhhhhh a lil note in the changelog would've saved me half a day of debugging. 😅 |
@benjamn I know this has been merged, but can we please get some support with this issue (re-opening it could be a start): #615 its been around for almost 2yrs with nobody looking into it. I know the Apollo guys have a lot of things going on with new features etc, but i just don't think SSR should be forgotten, and solving this issue will give Apollo the whole package, making it seamless to add SSR to an Apollo app, since all errors would be handled the exact same way. |
@OllieJennings I've re-opened #615. Thanks for the heads up! (and quick FYI - you @ mentioned the wrong Ben - easy to do since |
@hwillson cheers, yeah they look so similar, my bad there. |
`walkTree` is no longer used by `react-apollo` (see #2533), but since it was previously exported and made available externally, it is still available (albeit deprecated) until `react-apollo` 3.0. Unfortunately, the current `walkTree` implementation does not fully work with React's new Context API. It can be updated to work with modern versions of React, but the entire point of #2533 is to avoid spending anymore time working on `walkTree`, since its implementation relies on React internals (and is very brittle). Since external use of `walkTree` is very minimal, and since there have been no complaints of `walkTree` not working with React's new Context API since it has been made a standalone module (these problems were only noticed when `walkTree` was being used by `react-apollo`), I think we should leave `walkTree` as is. This commit makes sure `walkTree` passes Typescript compilation, but disables failing tests caused by render tree differences in newer versions of React.
The
getDataFromTree
function is used during server-side rendering (and sometimes also on the client) to await allfetchQuery
promises generated byQuery
components in a given tree of React components, so that subsequentReactDOM.renderToString
and/orrenderToStaticMarkup
calls can render the tree synchronously, without worrying about fetching data.Until now,
getDataFromTree
was implemented by walking the component tree, instantiating components along the way, fetching any data forQuery
components, callingrender()
methods, and recursively processing children. This simulated rendering process was similar to the official rendering process performed by React, but it was not exactly the same, so it created a nontrivial maintenance burden forreact-apollo
to accommodate any new React features.The recent introduction of Hooks is just one such feature, but hooks are particularly troublesome for
getDataFromTree
, because they depend intimately on internal React state that cannot be manipulated using any stable, public API. In other words, updating our tree-walking implementation ofgetDataFromTree
to support hooks would have been very difficult, if not impossible. Even if it was possible, it would never be truly future-proof, since there could always be other new React features that threatened doom forgetDataFromTree
.In general, the only way to make
getDataFromTree
truly future-proof is to implement it using the official React rendering functions, and/or wait for React to implement direct support for asynchronous server-side rendering (which would be AMAZING).For now, this PR implements
getDataFromTree
by callingrenderToStaticMarkup
repeatedly, collecting anyQuery
data promises and awaiting them between renderings. The number of renderings should never be greater than the length of the longest chain of nestedQuery
components (plus one), and the result of the final rendering is aPromise<string>
whose HTML value can be used immediately, instead of callingrenderToString
again.This approach is worse in some ways than our previous approach, since parts of the tree get re-rendered multiple times, and HTML is generated unnecessarily. Also, exceptions thrown during component rendering that are not related to data fetching can now abort the entire rendering, whereas previously we trapped errors within their specific subtrees. This was possible because we controlled the entire "rendering" process. Now that we're putting our faith in
ReactDOM
, we just have to roll with the punches, as far as exceptions are concerned. But that seems acceptable in practice.On the upside, we have a fully hooks-compatible implementation of
getDataFromTree
on the same day hooks were announced!