The Daily TIL

February 14, 2019 by AndyAndyjavascriptreactssr

ReactDOM.hydrate() is not ReactDOM.render()

When you’re using React SSR, initial client-side-rendered DOM structure must EXACTLY match server-side-rendered DOM. Otherwise the client-side-render will not be what you expect.

You CAN change innerText of nodes on the initial render but NOT structure/attributes. This is because components’ render funcs when in the context of hydrate() only attach event handlers. DOM reconciliation does not happen during hydrate().

So your React DOM will look like what you expect, but the browser DOM will not match the React DOM.

The correct way to do this, from React docs:

If you intentionally need to render something different on the server and the client, you can do a two-pass rendering. Components that render something different on the client can read a state variable like this.state.isClient, which you can set to true in componentDidMount(). This way the initial render pass will render the same content as the server, avoiding mismatches, but an additional pass will happen synchronously right after hydration. Note that this approach will make your components slower because they have to render twice, so use it with caution.

So basically, you need to force a state-triggered re-render right after hydrate() (using componentDidMount().

Caveat: Watch out for UI flicker if you do this. You’ll be able to see the original SSR’d content flash for a tick before re-render happens with client-side version.

Example

const SplitTestingThing = ({ experimentName, base, experiment, isClient }) => (
  <>
    {isClient ? (      // for client-side (post-didmount), render actual content
      <Experiment name={experimentName}>
        <Variant name={base.name}>{base.content}</Variant>
        <Variant name={experiment.name}>{experiment.content}</Variant>
      </Experiment>
    ) : (
      // for server-side and hydrate-pass, render skeleton/placeholder/default.
      base.content
    )}
  </>
);

const SplitTesting = compose(
  withState('isClient', 'setIsClient', false),
  lifecycle({
    componentDidMount() {      // trigger a full render/reconcile pass once mounted in browser dom      this.props.setIsClient(true);    },  })
)(SplitTestingThing);

References