React: How to prevent React from modifying elements manipulated by external code?

Created on 28 Sep 2017  Â·  10Comments  Â·  Source: facebook/react

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:

  • Server-side React renders the initial HTML for a graphic, using dangerouslySetInnerHTML, as part of a React page
  • Client-side React never touches those elements again, even during component mounting

We 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

for the map, as well as the map JS. On page load, the map is immediately drawn by D3 but gets erased when React mounts client-side. The D3 nodes aren’t included in the server-side output, because they often vary based on viewport, device, etc.

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:

  • We need to use dangerouslySetInnerHTML, because we need to deploy graphics outside of site releases
  • We need to render the initial HTML server-side for performance and simplicity
  • We can’t use iFrames for everything, as they’re not flexible enough
  • We can’t re-render or reattach elements, as that causes many problems — CSS animations restart, media playback is interrupted, input cursors and text selections are lost, etc.

Any guidance is appreciated.

cc @gaearon @leeb

Server Rendering Question

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.

All 10 comments

@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?

Was this page helpful?
0 / 5 - 0 ratings