Mounting React components in a different framework

eljenso

Jens Böttcher

Posted on June 26, 2019

Mounting React components in a different framework

The project we are working on started as a Backbone.js project, but we now began to integrate React into it.
This post is not about the reasoning behind that, but about something different:
how we use (or mount) React components inside a Backbone application.

When we write a new React app from scratch, we define our entrypoint component, usually called App, and mount it somewhere via ReactDOM into the existing DOM:
ReactDOM.render(<App />, document.getElementById("root"));.
We will then start to develop the application, which completely resides in that App component.

But this is not the case when we have an existing application written with another framework (in our case backbone), that we now want to use React inside it.
Our choices were to either:

  1. Rewrite the whole application from scratch
  2. Realize new features with React, and slowly replace Backbone.js code by React code in the process

For many reasons (which might be discussed in a future post), we chose option 2.


Lets define a new component that we want to integrate in our existing application:

function CounterButton() {
  // Define state using hooks
  const [count, setCount] = React.useState<number>(0);

  // Return button displaying current state and incrementing state on click
  return (
    <button onClick={
      () => setCount(count + 1)
    }>
      {count}
    </button>
  )
}

The CounterButton component renders a button that shows how often the user has clicked on it.
This component has a state count, initially set to 0, and the corresponding setter function setCount.

Now, in order to add CounterButton to our existing application at some place, we use ReactDOM.render to render it into an existing DOM element:
ReactDOM.render(<CounterButton />, document.getElementById("someElement"));.

And we are done!


Or so we thought.

What if you want to reuse the same component at the same place at a later time?
For example a modal (also known as dialogue), that the user closes at some point but might eventually open up again.

Let's add a show state to the CounterButton component, which can make the <button> disappear:

function CounterButton() {
  // Define state using hooks
  const [count, setCount] = React.useState(0);
  const [show, setShow] = React.useState(true);

  // Return button displaying current state and incrementing state on click
  if (!show) {
    return null;
  }
  return (
    <button onClick={
      () => {
        if (count === 5) {
          setShow(false);
        }
        setCount(count + 1);
      }
    }>
      {count}
    </button>
  )
}

CounterButton will now return null if !show yields true, completely removing <button> from the DOM when that show state changes from true to false.
Here, this is the case when count is 5 at the time the user clicks the button.

This logic is what we currently use to close a modal.
When the user triggers the close logic of that modal, we set the show state to false which result in the modal being removed from the DOM..

But what if you want to show CounterButton again after it disappeared?
Simply execute the following call again, right?
ReactDOM.render(<CounterButton />, document.getElementById("someElement"));
Sadly, CounterButton will not show up.

From the React docs:

If the React element was previously rendered into container, this will perform an update on it and only mutate the DOM as necessary to reflect the latest React element.

In other words, ReactDOM will render the same instance as before, only with updated props.
React will use the instance of CounterButton, that was previously used, with the same state: show is still false.

Our first idea to solve this issue was to create a new instance of CounterButton every time before we pass it to ReactDOM.render.
For this, we encapsulated the body of the CounterButton function inside an arrow function, essentially an anonymous functional component. CounterButton will now return this anonymous functional component:

function CounterButton() {
  return () => {
    // Define state using hooks
    const [count, setCount] = React.useState(0);
    const [show, setShow] = React.useState(true);

    // Return button displaying current state and incrementing state on click
    if (!show) {
      return null;
    }
    return (
      <button onClick={
        () => {
          if (count === 5) {
            setShow(false);
          }
          setCount(count + 1);
        }
      }>
        {count}
      </button>
    )
  }
}

// Create new functional component to pass into ReactDOM.render
const CounterButtonInstance = CounterButton();
ReactDOM.render(<CounterButtonInstance  />, document.getElementById("root"));

No matter how often we call ReactDOM.render with a return of CounterButton() into document.getElementById("root"), ReactDOM.render will always see this anonymous functional component as different component as the one before.
That is because it is a different anonymous functional component.

But this approach has at least one issue:
CounterButton is not a functional component anymore, but a function returning a functional component.
This makes reusing CounterButton inside a React application impossible.

Now, for our current solution, we removed that encapsulation introduced in the last code snippet.
Instead, we make use of the special component prop key, read more about it the React docs:

ReactDOM.render(
  <CounterButton key={new Date().getTime()} />, document.getElementById("root")
);

We make use of an important attribute of the key prop here: if React is about to re-render a component which has its key changed since the last render, React will discard that previous version and render it from scratch.
We use the current time (in milliseconds) as value for that prop; and since this will change between renders, React will create a new instance of CounterButton with a fresh state! 🎉

Below you see a codepen showcasing this approach.
Click that button a few times, and it will disappear to never come back again.
But if you uncomment those key props, CounterButton will get reset every 2 seconds.


Some afterthoughts

For that anonymous functional component, we could also had introduced another function that returns an anonymous functional returning the original CounterButton:

function CreateCounterButton() {
  return () => CounterButton()
}

Calling CreateCounterButton will then create a new instance of CounterButton on every call.
This will keep our CounterButton reusable.

Any of the approaches described above have a drawback:
CounterButton will still be part of the ReactDOM, even after its removed from the DOM.
We should make sure that CounterButton is properly unmounted from the ReactDOM once it is not used anymore; otherwise, it can be considered a memory leak, which can result in performance issues.
ReactDOM provides an unmountComponentAtNode(container) method, which allows to unmount any React component mounted in the container.

In our example, we would utilize it like this:

ReactDOM.unmountComponentAtNode(document.getElementById("root"))

But since CounterButton is not, and should not be, aware that it has to be unmounted this way, that call should be handled from the outside.

We did not look further into using unmountComponentAtNode yet.
Since we do not have many React components yet (we currently have around 40 tsx files in the codebase), the key prop approach seems sufficient.
We should look further into this approach once the think that leaving unused components in the ReactDOM affects the performance of our application.

💖 💪 🙅 🚩
eljenso
Jens Böttcher

Posted on June 26, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related