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:
- Rewrite the whole application from scratch
- 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.
Top comments (0)