Play with React Concurrent Mode with Your Gatsby Site [updated with more proper solution]
Tags: react, gatsby
Published: 2019-10-28 16:06:43


So the React team released [curious cat version for concurrent mode](https://reactjs.org/docs/concurrent-mode-intro.html), and I want to try that with my personal sites and side projects, only to realize that by using Gatsby I do not have direct access to my `ReactDOM.render()`, which I am supposed to change.

<!-- excerpt ends -->

## TL;DR

Put in your `gatsby-browser.js` the following:

Solution by [Fredrik Höglund](https://twitter.com/EphemeralCircle):

```js
// gatsby-browser.js
const ReactDOM = require('react-dom');

exports.replaceHydrateFunction = () => {
  return (element, container, callback) => {
    ReactDOM.createRoot(container, {
      hydrate: true,
      hydrationOptions: { onHydrated: callback },
    }).render(element);
  };
};
```

## Notes

A quick search landed me on [this issue](https://github.com/gatsbyjs/gatsby/issues/13675), which brings me to [Gatsby's Browser APIs](https://www.gatsbyjs.org/docs/browser-apis/). And in particular, its [`replaceHydrateFunction`](https://www.gatsbyjs.org/docs/browser-apis/#replaceHydrateFunction). This function is meant for customized hydration on SSR. It just so happens that it becomes our chance to swap out the `ReactDOM.render()` call. [Gatsby will call what we return as the replacement](https://github.com/gatsbyjs/gatsby/blob/ebff3153364974a3893b3c943b311c03868b8fcb/packages/gatsby/cache-dir/production-app.js#L152).

```js
exports.replaceHydrateFunction = () => {
  return (element, container, callback) => {
    console.log('rendering!');
    ReactDOM.render(element, container, callback);
  };
};
```

And from React's official docs on Concurrent Mode, this is what we should change:

```jsx
import ReactDOM from 'react-dom';

// If you previously had:
// ReactDOM.render(<App />, document.getElementById('root'));
// You can opt into Concurrent Mode by writing:

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
```

My initial attempt was to write my tweak like this:

```js
exports.replaceHydrateFunction = () => {
  return (element, container) => {
    ReactDOM.createRoot(container).render(element);
  };
};
```

It works. But on local build only. On production build, my page content gets duplicated, as you can see [in this preview](https://deploy-preview-59--react-knowledgeable.netlify.com).

<img src="./duplicated dom on production build.png" />

~~Out of my amateurish understanding around Gatsby, this looks like a [DOM hydration](https://www.gatsbyjs.org/docs/production-app/#dom-hydration) issue. I now have a blurry understanding about why this following code works, but I probably should not be misleading people. If anybody has a better understanding, please [teach me](https://twitter.com/wgao19) 🙆🏻‍♀️~~

I later on realize the problem was probably due to not calling `ReactDOM.hydrate`, which is supposed to be the default behavior. Below has been updated to _some_ code that works:

```js
exports.replaceHydrateFunction = () => {
  return (element, container, callback) => {
    ReactDOM.hydrate(element, container, callback);
    ReactDOM.createRoot(container).render(
      process.env.NODE_ENV === 'production' ? callback(element) : element
    );
  };
};
```

But [Fredrik Höglund](https://twitter.com/EphemeralCircle) pointed out that this is problematic, because

> In legacy mode, ReactDOM.render and ReactDOM.hydrate are two separate functions. In concurrent mode, there is only one function, ReactDOM.createRoot, but with an option for hydrate.
> [Src if curious](https://github.com/facebook/react/blob/f6b8d31a76cbbcbbeb2f1d59074dfe72e0c82806/packages/react-dom/src/client/ReactDOM.js#L619)

So a more proper fix will be as follows:

```js
exports.replaceHydrateFunction = () => {
  return (element, container, callback) => {
    ReactDOM.createRoot(container, {
      hydrate: true,
      hydrationOptions: { onHydrated: callback },
    }).render(element);
  };
};
```

And you can [check out the twitter discussion](https://twitter.com/EphemeralCircle/status/1190671700645597184?s=20).

## More notes

Don't read these.

### When and how does Gatsby call `render`?

It calls the `renderer` which is the return of `replaceHydrateFunction`, defaults to `ReactDOM.hydrate`. Then, it will call `onInitialClientRender`, which is the third parameter, `callback`, in the return function of `replaceHydrateFunction`.

### What does `ReactDOM.hydrate` do in a Gatsby site?

According to the [Gatsby documentation on DOM hydration](https://www.gatsbyjs.org/docs/production-app/#dom-hydration):

> [`hydrate()`](https://reactjs.org/docs/react-dom.html#hydrate) is a [ReactDOM](https://reactjs.org/docs/react-dom.html) function which is the same as `render()`, except that instead of generating a new DOM tree and inserting it into the document, it expects that a React DOM already exists with exactly the same structure as the React Model. It therefore descends this tree and attaches the appropriate event listeners to it so that it becomes a live React DOM. Since your HTML was rendered with exactly the same code as you're running in your browser, these will (and have to) match perfectly.

Checking our Gatsby site's `public` directory, those are some uglified HTMLs.

> The hydration occurs on the `<div id="___gatsby">...</div>` element defined in [default-html.js](https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/default-html.js#L21).

But I still don't understand what caused the duplication above -.- It duplicates the child element of `<div id="___gatsby">...</div>`.

### What happens if the DOM isn't properly hydrated or `onInitialClientRender` isn't properly called?

- event handlers don't get attached
- css-in-js doesn't get inserted

maybe more

## Links

- [Adopting Concurrent Mode (Experimental)](https://reactjs.org/docs/concurrent-mode-adoption.html)
- [Extend BrowserApi for React Concurrent Mode #13675](https://github.com/gatsbyjs/gatsby/issues/13675)
- [GatsbyJS Browser API `replaceHydrateFunction`](https://www.gatsbyjs.org/docs/browser-apis/#replaceHydrateFunction)
- [Gatsby HTML hydration](https://www.gatsbyjs.org/docs/html-generation/#7-render-final-html-document)
- [ReactDOM hydrate doc](https://reactjs.org/docs/react-dom.html#hydrate)
- [twitter thread about this](https://twitter.com/EphemeralCircle/status/1190671700645597184?s=20)