JSX was a Mistake
It wasn’t. I just wanted your attention. Thanks.
And now while I have it, I do want to discuss a bit of common confusion caused by JSX.
One of the most common misunderstandings I run into with people regarding React is this: If a component has a state change and rerenders, it does not cause children
to rerender. I think this is very obvious when we remove JSX, and use React.createElement
instead. Let’s have a look.
Here we have one of my favorite components to demonstrate rerenders, a Box
that changes background color whenever it’s rendered. This Box
allows us to pass children
through it. Thus, we’ll make an Example
where we force an update, which will cause Box
to rerender.
import { useForceUpdate } from './hooks'
import { randomRGB } from './utils'
function Box({ children }) {
return (
<div style={{ backgroundColor: randomRGB(), padding: '1rem' }}>
{children}
</div>
)
}
function Example() {
const forceUpdate = useForceUpdate()
return (
<Box>
<button type="button" onClick={forceUpdate}>
Update
</button>
</Box>
)
}
As you can see, clicking “Update” causes the Box
to change colors. This is because Box
is a function that gets called when Example
is called. This is obvious when instead of JSX, we use React.createElement
instead.
function Box({ children }) {
return React.createElement('div', {
style: { backgroundColor: randomRGB(), padding: '1rem' },
children,
})
}
function Example() {
const forceUpdate = useForceUpdate()
return React.createElement(
Box,
null,
React.createElement('button', { type: 'button', onClick: forceUpdate }),
)
}
Notice that this time, we replaced <div />
with React.createElement('div')
. Then when we needed to render the Box
in Example
, we pass it to another call of React.createElement
. Thus, each time Example
rerenders, we call a function to render a Box
and a function to render a button
. Makes it pretty clear where functions are getting called.
How does this relate to children
?
Well, simply put, children
is typically not a function call, it’s just a value.
What if I add a “slot” for children
in our Example
component like this:
function Example({ children }) {
const forceUpdate = useForceUpdate()
return React.createElement(
Box,
null,
// createElement's third parameter is a rest parameter,
// so we can add more items by adding more arguments
React.createElement('button', { type: 'button', onClick: forceUpdate }),
// here's the slot, it'll come after our button
children,
)
}
Now, what happens if we pass Box
as children
of our Example
?
<Example>
<Box>I am a child</Box>
<Example>
Notice that our child Box
doesn’t rerender, while the parent Box
still does. It remains the same background color it was when it initially rendered, because it’s not getting called again when Example
rerenders.
Heck, let’s have some fun and put Example
into Example
.
Kind of fun, isn’t it?
When does this matter?
In most cases, I wouldn’t fret about rerendering a Box
or whatever component you might be working with, but it’s just good knowledge to have. You’ll be better off having this concept etched into your brain than not.
The place I’m most concerned about getting this right is the use of Context Providers. I wrote a very similar blog post all about that. Check it out if you’re interested.
In general, if you have a component with frequent state changes and it uses children
, consider letting as much of the inner contents be composed via children
as possible to avoid unnecessary rendering.
You never know, you might just find your component is now a bit more reusable, too.