The New York Times is rebuilding its website using React. Currently, it’s an isomorphic app that has both server- and client-side renders.
Our question: What's the best way to include a non-React interactive graphic — maps, charts and other visualizations created by custom code — within a fully React page?
Our ideal scenario:
dangerouslySetInnerHTML
, as part of a React pageWe thought React 16 might solve this with hydrate
method, but it still removes nodes that it doesn't expect, such as nodes generated by D3 or other client-side code. None of the options in Integrating with Other Libraries seem to be an exact match either.
The classic use case is a graphic with a D3 map. The server-side HTML includes text and a placeholder
Here’s a trivial example, showing React 16’s hydrate
removing client-created nodes after one second. On mobile devices, loading the React library and potentially other dependencies could take some seconds.
The simplest solution I can think of is a shouldComponentMount
function, where we could return false. The rest of the React components on the page would mount, but leave the interactive graphic part alone. There are probably other solutions.
Constraints:
Any guidance is appreciated.
cc @gaearon @leeb
@giratikanon just throwing something out there. I'm not as close to the use cases you mentioned as you are so you could sense check this, but would something like the following pattern work?
class OnlyOnClient extends Component {
static propTypes = {
placeholder: PropTypes.node,
html: PropTypes.string
};
state = {
onClient: false
};
shouldComponentUpdate() {
return !this.state.onClient;
}
componentDidMount() {
this.setState({
onClient: true
});
}
render() {
if (!this.state.onClient) {
return this.props.placeholder;
}
return <div dangerouslySetInnerHTML={{ __html: this.props.html }} />;
}
}
const Graphics = () => (
<div>
This text can re-render
<OnlyOnClient
placeholder={
<div style={{ height: 200 }}>
Try to reserve best guess height, placeholder graphic, etc
</div>
}
html={"<div>d3 graphics etc</div>"} // this won't re-render
/>
Okay to re-render here too
</div>
);
The idea being you wrap the key parts of the post that need to avoid being re-rendered on the client (by deferring the server-rendering to one-time on the client) using <OnlyOnClient />
. Use a placeholder with a best-guess height or an appropriate loader graphic to avoid reflow and improve the reading experience.
I haven't really tested this, so it's possible I'm overlooking something obvious/doing something dumb here 🙈
Triggering a state change in componentDidMount
like suggested in https://github.com/facebook/react/issues/10923#issuecomment-332960759 is the only existing solution I’m aware of. Have you bumped into any issues with it?
React executes setState
in componentDidMount
synchronously precisely to accommodate use cases like this.
As a work around you can set dangerouslySetInnerHTML to something else on the client. Like blank content or a space. We won't try to manipulate the tree of a dangerouslySetInnerHTML node on the client. Even if it is wrong. That will still trigger a warning but 16.1 will have a way to suppress it.
Definitely hacky though.
@sebmarkbage hacky as it may be, we will definitely try it, thanks!
Suggestion in https://github.com/facebook/react/issues/10923#issuecomment-332960759 worked fine out of the box. In my case I'm using heavy / rich widgets that doesn't support container update / removal or unattach. Particularly monaco-editor and heavy weight visualizations using d3, or big data-tables trees etc, that cannot be re-crated each time state changes. This has nothing to do with server side rendering but still the snippet is generic enough to work out of the box in my situation too. Wonder if react has now a more elegant/formal way of handling with this problem, perhaps hooks ? Thanks!
this workaround saved me!! thanks. would be very nice if this could be used as a decorator or prop/hook on the component! thx again @tizmagik and @sebmarkbage
As a work around you can set dangerouslySetInnerHTML to something else on the client. Like blank content or a space. We won't try to manipulate the tree of a dangerouslySetInnerHTML node on the client. Even if it is wrong. That will still trigger a warning but 16.1 will have a way to suppress it.
Definitely hacky though.
I wonder if the React team considers this part of React's API or if it's just a "lucky accident" which might be fixed in the future?
Put differently, can I rely on this not silently (in a minor update) changing in the future or should I create a PR that adds documentation for it? 🙃
It seems that re-rendering components in client-side still doesn't affect their in-line styles (that were calculated in the Server-side). so after hydrating and rendering the styles stay the same. This is a problem when the lack of window in the server-side requires a "best guess" of width and height but this guess will not update to the correct one after new rendering cycles.
@sebmarkbage
As a work around you can set dangerouslySetInnerHTML to something else on the client. Like blank content or a space. We won't try to manipulate the tree of a dangerouslySetInnerHTML node on the client. Even if it is wrong. That will still trigger a warning but 16.1 will have a way to suppress it.
Looks like it works for hydration, but does not work for (re)render.
In our case we have a components tree:
<LazyRender id='outer'>
<div>Outer
<LazyRender id='inner'>
<span>Inner</span>
</LazyRender>
</div>
</LazyRender>
Each component in that tree starts to load when it is visible. But it is not possible to say, whether Inner is visible or not, until Outer component is rendered.
When Outer component rendered, Inner component disappear, until it is loaded and rendered itself. So, we have flickering of all non-first level components in our tree.
Probably we will look towards loading all subtree components at once, when first-level component become visible, but that is a bit tricky in our case...
As a work around you can set dangerouslySetInnerHTML to something else on the client. Like blank content or a space. We won't try to manipulate the tree of a dangerouslySetInnerHTML node on the client. Even if it is wrong. That will still trigger a warning but 16.1 will have a way to suppress it.
Definitely hacky though.
I am using react 16.8.6 and for me dangerouslySetInnerHTML gets hydrated on client. It renders empty string.
<div dangerouslySetInnerHTML={{ __html: " " }} />
Is it started hydrating in react 16.8.6?
Most helpful comment
As a work around you can set dangerouslySetInnerHTML to something else on the client. Like blank content or a space. We won't try to manipulate the tree of a dangerouslySetInnerHTML node on the client. Even if it is wrong. That will still trigger a warning but 16.1 will have a way to suppress it.
Definitely hacky though.