https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/
Mark's Dev Blog
* About
Random musings on React, Redux, and more, by Redux maintainer Mark
"acemarke" Erikson
Sponsor @markerikson
Home
Blogged Answers: A (Mostly) Complete Guide to React Rendering
Behavior
Posted on May 17, 2020
#react #context #redux #greatest-hits
This is a post in the Blogged Answers series.
---------------------------------------------------------------------
Details on how React rendering behaves, and how use of Context and
React-Redux affect rendering
I've seen a lot of ongoing confusion over when, why, and how React
will re-render components, and how use of Context and React-Redux
will affect the timing and scope of those re-renders. After having
typed up variations of this explanation dozens of times, it seems
it's worth trying to write up a consolidated explanation that I can
refer people to. Note that all this information is available online
already, and has been explained in numerous other excellent blog
posts and articles, several of which I'm linking at the end in the
"Further Information" section for reference. But, people seem to be
struggling to put the pieces together for a full understanding, so
hopefully this will help clarify things for someone.
Note: Updated October 2022 to cover React 18 and future React
updates
I also did a talk based on this post for React Advanced 2022:
React Advanced 2022 - A (Brief) Guide to React Rendering Behavior
Table of Contents [?]
* What is "Rendering"?
+ Rendering Process Overview
+ Render and Commit Phases
* How Does React Handle Renders?
+ Queuing Renders
+ Standard Render Behavior
+ Rules of React Rendering
+ Component Metadata and Fibers
+ Component Types and Reconciliation
+ Keys and Reconciliation
+ Render Batching and Timing
+ Async Rendering, Closures, and State Snapshots
+ Render Behavior Edge Cases
* Improving Rendering Performance
+ Component Render Optimization Techniques
+ How New Props References Affect Render Optimizations
+ Optimizing Props References
+ Memoize Everything?
+ Immutability and Rerendering
+ Measuring React Component Rendering Performance
* Context and Rendering Behavior
+ Context Basics
+ Updating Context Values
+ State Updates, Context, and Re-Renders
+ Context Updates and Render Optimizations
* React-Redux and Rendering Behavior
+ React-Redux Subscriptions
+ Differences between connect and useSelector
* Future React Improvements
+ "React Forget" Memoizing Compiler
+ Context Selectors
* Summary
* Final Thoughts
* Further Information
What is "Rendering"? [?]
Rendering is the process of React asking your components to describe
what they want their section of the UI to look like, now, based on
the current combination of props and state.
Rendering Process Overview [?]
During the rendering process, React will start at the root of the
component tree and loop downwards to find all components that have
been flagged as needing updates. For each flagged component, React
will call either FunctionComponent(props) (for function components),
or classComponentInstance.render() (for class components) , and save
the render output for the next steps of the render pass.
A component's render output is normally written in JSX syntax, which
is then converted to React.createElement() calls as the JS is
compiled and prepared for deployment. createElement returns React
elements, which are plain JS objects that describe the intended
structure of the UI. Example:
// This JSX syntax:
return Text here
// is converted to this call:
return React.createElement(MyComponent, {a: 42, b: "testing"}, "Text Here")
// and that becomes this element object:
{type: MyComponent, props: {a: 42, b: "testing"}, children: ["Text Here"]}
// And internally, React calls the actual function to render it:
let elements = MyComponent({...props, children})
// For "host components" like HTML:
return
// becomes
React.createElement("button", {onClick}, "Click Me")
// and finally:
{type: "button", props: {onClick}, children: ["Click me"]}
After it has collected the render output from the entire component
tree, React will diff the new tree of objects (frequently referred to
as the "virtual DOM"), and collects a list of all the changes that
need to be applied to make the real DOM look like the current desired
output. The diffing and calculation process is known as
"reconciliation".
React then applies all the calculated changes to the DOM in one
synchronous sequence.
Note: The React team has downplayed the term "virtual DOM" in
recent years. Dan Abramov said:
I wish we could retire the term "virtual DOM". It made sense in
2013 because otherwise people assumed React creates DOM nodes on
every render. But people rarely assume this today. "Virtual DOM"
sounds like a workaround for some DOM issue. But that's not what
React is.
React is "value UI". Its core principle is that UI is a value,
just like a string or an array. You can keep it in a variable,
pass it around, use JavaScript control flow with it, and so on.
That expressiveness is the point -- not some diffing to avoid
applying changes to the DOM.
It doesn't even always represent the DOM, for example is not DOM. Conceptually it represents lazy
function calls: Message.bind(null, { recipientId: 10 }).
Render and Commit Phases [?]
The React team divides this work into two phases, conceptually:
* The "Render phase" contains all the work of rendering components
and calculating changes
* The "Commit phase" is the process of applying those changes to
the DOM
After React has updated the DOM in the commit phase, it updates all
refs accordingly to point to the requested DOM nodes and component
instances. It then synchronously runs the componentDidMount and
componentDidUpdate class lifecycle methods, and the useLayoutEffect
hooks.
React then sets a short timeout, and when it expires, runs all the
useEffect hooks. This step is also known as the "Passive Effects"
phase.
React 18 added "Concurrent Rendering" features like useTransition.
This gives React the ability to pause the work in the rendering phase
to allow the browser to process events. React will either resume,
throw away, or recalculate that work later as appropriate. Once the
render pass has been completed, React will still run the commit phase
synchronously in one step.
A key part of this to understand is that "rendering" is not the same
thing as "updating the DOM", and a component may be rendered without
any visible changes happening as a result. When React renders a
component:
* The component might return the same render output as last time,
so no changes are needed
* In Concurrent Rendering, React might end up rendering a component
multiple times, but throw away the render output each time if
other updates invalidate the current work being done
This excellent interactive React hooks timeline diagram helps
illustrate the sequencing of rendering, committing, and executing
hooks:
React hooks timeline diagram
For additional visualizations, see:
* React hooks flow diagram
* React hooks render/commit phase diagram
* React class lifecycle methods diagram
How Does React Handle Renders? [?]
Queuing Renders [?]
After the initial render has completed, there are a few different
ways to tell React to queue a re-render:
* Function components:
+ useState setters
+ useReducer dispatches
* Class components:
+ this.setState()
+ this.forceUpdate()
* Other:
+ Calling the ReactDOM top-level render() method again
(which is equivalent to calling forceUpdate() on the root
component)
+ Updates triggered from the new useSyncExternalStore hook
Note that function components don't have a forceUpdate method, but
you can get the same behavior by using a useReducer hook that always
increments a counter:
const [, forceRender] = useReducer((c) => c + 1, 0);
Standard Render Behavior [?]
It's very important to remember that:
React's default behavior is that when a parent component renders,
React will recursively render all child components inside of it!
As an example, say we have a component tree of A > B > C > D, and
we've already shown them on the page. The user clicks a button in B
that increments a counter:
* We call setState() in B, which queues a re-render of B.
* React starts the render pass from the top of the tree
* React sees that A is not marked as needing an update, and moves
past it
* React sees that B is marked as needing an update, and renders it.
B returns as it did last time.
* C was not originally marked as needing an update. However,
because its parent B rendered, React now moves downwards and
renders C as well. C returns again.
* D was also not marked for rendering, but since its parent C
rendered, React moves downward and renders D too.
To repeat this another way:
Rendering a component will, by default, cause all components inside
of it to be rendered too!
Also, another key point:
In normal rendering, React does not care whether "props changed" - it
will render child components unconditionally just because the parent
rendered!
This means that calling setState() in your root component, with
no other changes altering the behavior, will cause React to re-render
every single component in the component tree. After all, one of the
original sales pitches for React was "act like we're redrawing the
entire app on every update".
Now, it's very likely that most of the components in the tree will
return the exact same render output as last time, and therefore React
won't need to make any changes to the DOM. But, React will still have
to do the work of asking components to render themselves and diffing
the render output. Both of those take time and effort.
Remember, rendering is not a bad thing - it's how React knows whether
it needs to actually make any changes to the DOM!
Rules of React Rendering [?]
One of the primary rules of React rendering is that rendering must be
"pure" and not have any side effects!
This can be tricky and confusing, because many side effects are not
obvious, and don't result in anything breaking. For example, strictly
speaking a console.log() statement is a side effect, but it won't
actually break anything. Mutating a prop is definitely a side effect,
and it might not break anything. Making an AJAX call in the middle of
rendering is also definitely a side effect, and can definitely cause
unexpected app behavior depending on the type of request.
Sebastian Markbage wrote an excellent document entitled The Rules of
React. In it, he defines the expected behaviors for different React
lifecycle methods, including render, and what kinds of operations
would be considered safely "pure" and which would be unsafe. It's
worth reading that in its entirety, but I'll summarize the key
points:
* Render logic must not:
+ Can't mutate existing variables and objects
+ Can't create random values like Math.random() or Date.now()
+ Can't make network requests
+ Can't queue state updates
* Render logic may:
+ Mutate objects that were newly created while rendering
+ Throw errors
+ "Lazy initialize" data that hasn't been created yet, such as
a cached value
Component Metadata and Fibers [?]
React stores an internal data structure that tracks all the current
component instances that exist in the application. The core piece of
this data structure is an object called a "fiber", which contains
metadata fields that describe:
* What component type is supposed to be rendered at this point in
the component tree
* The current props and state associated with this component
* Pointers to parent, sibling, and child components
* Other internal metadata that React uses to track the rendering
process
If you've ever heard the phrase "React Fiber" used to describe a
React version or feature, that's really referring to the rewrite of
React's internals that switched the rendering logic to rely on these
"Fiber" objects as the key data structure. That was released as React
16.0, so every version of React since then has used this approach.
The shortened version of the Fiber type looks like:
export type Fiber = {
// Tag identifying the type of fiber.
tag: WorkTag;
// Unique identifier of this child.
key: null | string;
// The resolved function/class/ associated with this fiber.
type: any;
// Singly Linked List Tree Structure.
child: Fiber | null;
sibling: Fiber | null;
index: number;
// Input is the data coming into this fiber (arguments/props)
pendingProps: any;
memoizedProps: any; // The props used to create the output.
// A queue of state updates and callbacks.
updateQueue: Array;
// The state used to create the output
memoizedState: any;
// Dependencies (contexts, events) for this fiber, if any
dependencies: Dependencies | null;
};
(You can see the full definition of the Fiber type as of React 18
here.)
During a rendering pass, React will iterate over this tree of fiber
objects, and construct an updated tree as it calculates the new
rendering results.
Note that these "fiber" objects store the real component props and
state values. When you use props and state in your components, React
is actually giving you access to the values that were stored on the
fiber objects. In fact, for class components specifically, React
explicitly copies componentInstance.props = newProps over to the
component right before rendering it. So, this.props does exist, but
it only exists because React copied the reference over from its
internal data structures. In that sense, components are sort of a
facade over React's fiber objects.
Similarly, React hooks work because React stores all of the hooks for
a component as a linked list attached to that component's fiber
object. When React renders a function component, it gets that linked
list of hook description entries from the fiber, and every time you
call another hook, it returns the appropriate values that were stored
in the hook description object (like the state and dispatch values
for useReducer.
When a parent component renders a given child component for the first
time, React creates a fiber object to track that "instance" of a
component. For class components, it literally calls const instance =
new YourComponentType(props) and saves the actual component instance
onto the fiber object. For function components, React just calls
YourComponentType(props) as a function.
Component Types and Reconciliation [?]
As described in the "Reconciliation" docs page, React tries to be
efficient during re-renders, by reusing as much of the existing
component tree and DOM structure as possible. If you ask React to
render the same type of component or HTML node in the same place in
the tree, React will reuse that and just apply updates if
appropriate, instead of re-creating it from scratch. That means that
React will keep component instances alive as long as you keep asking
React to render that component type in the same place. For class
components, it actually does use the same actual instance of your
component. A function component has no true "instance" the way a
class does, but we can think of as
representing an "instance" in terms of "a component of this type is
being shown here and kept alive".
So, how does React know when and how the output has actually changed?
React's rendering logic compares elements based on their type field
first, using === reference comparisons. If an element in a given spot
has changed to a different type, such as going from
to
or to , React will speed up the comparison
process by assuming that entire tree has changed. As a result, React
will destroy that entire existing component tree section, including
all DOM nodes, and recreate it from scratch with new component
instances.
This means that you must never create new component types while
rendering! Whenever you create a new component type, it's a different
reference, and that will cause React to repeatedly destroy and
recreate the child component tree.
In other words, don't do this:
// BAD!
// This creates a new `ChildComponent` reference every time!
function ParentComponent() {
function ChildComponent() {
return
Hi
;
}
return ;
}
Instead, always define components separately:
// GOOD
// This only creates one component type reference
function ChildComponent() {
return
Hi
;
}
function ParentComponent() {
return ;
}
Keys and Reconciliation [?]
The other way that React identifies component "instances" is via the
key pseudo-prop. React uses key as a unique identifier that it can
use to differentiate specific instances of a component type.
Note that key isn't actually a real prop - it's an instruction to
React. React will always strip that off, and it will never be passed
through to the actual component, so you can never have props.key - it
will always be undefined.
The main place we use keys is rendering lists. Keys are especially
important here if you are rendering data that may be changed in some
way, such as reordering, adding, or deleting list entries. It's
particularly important here that keys should be some kind of unique
IDs from your data if at all possible - only use array indices as
keys as a last resort fallback!
// Use a data object ID as the key for list items
todos.map((todo) => );
Here's an example of why this matters. Say I render a list of 10
components, using array indices as keys. React sees 10
items, with keys of 0..9. Now, if we delete items 6 and 7, and add
three new entries at the end, we end up rendering items with keys of
0..10. So, it looks to React like I really just added one new entry
at the end because we went from 10 list items to 11. React will
happily reuse the existing DOM nodes and component instances. But,
that means that we're probably now rendering
with the todo item that was being passed to list item #8. So, the
component instance is still alive, but now it's getting a different
data object as a prop than it was previously. This may work, but it
may also produce unexpected behavior. Also, React will now have to go
apply updates to several of the list items to change the text and
other DOM contents, because the existing list items are now having to
show different data than before. Those updates really shouldn't be
necessary here, since none of those list items changed.
If instead we were using key={todo.id} for each list item, React will
correctly see that we deleted two items and added three new ones. It
will destroy the two deleted component instances and their associated
DOM, and create three new component instances and their DOM. This is
better than having to unnecessarily update the components that didn't
actually change.
Keys are useful for component instance identity beyond lists as well.
You can add a key to any React component at any time to indicate its
identity, and changing that key will cause React to destroy the old
component instance and DOM and create new ones. A common use case for
this is a list + details form combination, where the form shows the
data for the currently selected list item. Rendering will cause React to destroy and re-create the form
when the selected item changes, thus avoiding any issues with stale
state inside the form.
Render Batching and Timing [?]
By default, each call to setState() causes React to start a new
render pass, execute it synchronously, and return. However, React
also applies a sort of optimization automatically, in the form of
render batching. Render batching is when multiple calls to setState()
result in a single render pass being queued and executed, usually on
a slight delay.
The React community often describes this as "state updates may be
asynchronous". The new React docs also describe it as "State is a
Snapshot". That's a reference to this render batching behavior.
In React 17 and earlier, React only did batching in React event
handlers such as onClick callbacks. Updates queued outside of event
handlers, such as in a setTimeout, after an await, or in a plain JS
event handler, were not queued, and would each result in a separate
re-render.
However, React 18 now does "automatic batching" of all updates queued
in any single event loop tick. This helps cut down on the overall
number of renders needed.
Let's look at a specific example.
const [counter, setCounter] = useState(0);
const onClick = async () => {
setCounter(0);
setCounter(1);
const data = await fetchSomeData();
setCounter(2);
setCounter(3);
};
With React 17, this executed three render passes. The first pass will
batch together setCounter(0) and setCounter(1), because both of them
are occurring during the original event handler call stack, and so
they're both occurring inside the unstable_batchedUpdates() call.
However, the call to setCounter(2) is happening after an await. This
means the original synchronous call stack is done, and the second
half of the function is running much later in a totally separate
event loop call stack. Because of that, React will execute an entire
render pass synchronously as the last step inside the setCounter(2)
call, finish the pass, and return from setCounter(2).
The same thing will then happen for setCounter(3), because it's also
running outside the original event handler, and thus outside the
batching.
However, with React 18, this executes two render passes. The first
two, setCounter(0) and setCounter(1), are batched together because
they're in one event loop tick. Later, after the await, both
setCounter(2) and setCounter(3) are batched together - even though
they're much later, that's also two state updates queued in the same
event loop, so they get batched into a second render.
Async Rendering, Closures, and State Snapshots [?]
One extremely common mistake we see all the time is when a user sets
a new value, then tries to log the existing variable name. However,
the original value gets logged, not the updated value.
function MyComponent() {
const [counter, setCounter] = useState(0);
const handleClick = () => {
setCounter(counter + 1);
// THIS WON'T WORK!
console.log(counter);
// Original value was logged - why is this not updated yet??????
};
}
So, why doesn't this work?
As mentioned above, it's common for experienced users to say "React
state updates are async". This is sort of true, but there's a bit
more nuance then that, and actually a couple different problems at
work here.
Strictly speaking, the React render is literally synchronous - it
will be executed in a "microtask" at the very end of this event loop
tick. (This is admittedly being pedantic, but the goal of this
article is exact details and clarity.) However, yes, from the point
of view of that handleClick function, it's "async" in that you can't
immediately see the results, and the actual update occurs much later
than the setCounter() call.
However, there's a bigger reason why this doesn't work. The
handleClick function is a "closure" - it can only see the values of
variables as they existed when the function was defined. In other
words, these state variables are a snapshot in time.
Since handleClick was defined during the most recent render of this
function component, it can only see the value of counter as it
existed during that render pass. When we call setCounter(), it queues
up a future render pass, and that future render will have a new
counter variable with the new value and a new handleClick function...
but this copy of handleClick will never be able to see that new
value.
The new React docs cover this in more detail in the section State as
a Snapshot, which is highly recommended reading.
Going back to the original example: trying to use a variable right
after you set an updated value is almost always the wrong approach,
and suggests you need to rethink how you are trying to use that
value.
Render Behavior Edge Cases [?]
Commit Phase Lifecycles [?]
There's some additional edge cases inside of the commit-phase
lifecycle methods: componentDidMount, componentDidUpdate, and
useLayoutEffect. These largely exist to allow you to perform
additional logic after a render, but before the browser has had a
chance to paint. In particular, a common use case is:
* Render a component the first time with some partial but
incomplete data
* In a commit-phase lifecycle, use refs to measure the real size of
the actual DOM nodes in the page
* Set some state in the component based on those measurements
* Immediately re-render with the updated data
In this use case, we don't want the initial "partial" rendered UI to
be visible to the user at all - we only want the "final" UI to show
up. Browsers will recalculate the DOM structure as it's being
modified, but they won't actually paint anything to the screen while
a JS script is still executing and blocking the event loop. So, you
can perform multiple DOM mutations, like div.innerHTML = "a";
div.innerHTML = b";, and the "a" will never appear.
Because of this, React will always run renders in commit-phase
lifecycles synchronously. That way, if you do try to perform an
update like that "partial->final" switch, only the "final" content
will ever be visible on screen.
As far as I know, state updates in useEffect callbacks are queued up,
and flushed at the end of the "Passive Effects" phase once all the
useEffect callbacks have completed.
Reconciler Batching Methods [?]
React reconcilers (ReactDOM, React Native) have methods to alter
render batching.
For React 17 and earlier, you can wrap multiple updates that are
outside of event handlers in unstable_batchedUpdates() to batch them
together. (Note that despite the unstable_ prefix, it's heavily used
and depended on by code at Facebook and public libraries -
React-Redux v7 used unstable_batchedUpdates internally)
Since React 18 automatically batches by default, React 18 has a
flushSync() API that you can use to force immediate renders and opt
out of automatic batching.
Note that since these are reconciler-specific APIs, alternate
reconcilers like react-three-fiber and ink may not have them exposed.
Check the API declarations or implementation details to see what's
available.
[?]
React will double-render components inside of a tag in
development. That means the number of times your rendering logic runs
is not the same as the number of committed render passes, and you
cannot rely on console.log() statements while rendering to count the
number of renders that have occurred. Instead, either use the React
DevTools Profiler to capture a trace and count the number of
committed renders overall, or add logging inside of a useEffect hook
or componentDidMount/Update lifecycle. That way the logs will only
get printed when React has actually completed a render pass and
committed it.
Setting State While Rendering [?]
In normal situations, you should never queue a state update while in
the actual rendering logic. In other words, it's fine to create a
click callback that will call setSomeState() when the click happens,
but you should not call setSomeState() as part of the actual
rendering behavior.
However, there is one exception to this. Function components may call
setSomeState() directly while rendering, as long as it's done
conditionally and isn't going to execute every time this component
renders. This acts as the function component equivalent of
getDerivedStateFromProps in class components. If a function component
queues a state update while rendering, React will immediately apply
the state update and synchronously re-render that one component
before moving onwards. If the component infinitely keeps queueing
state updates and forcing React to re-render it, React will break the
loop after a set number of retries and throw an error (currently 50
attempts). This technique can be used to immediately force an update
to a state value based on a prop change, without requiring a
re-render + a call to setSomeState() inside of a useEffect.
Improving Rendering Performance [?]
Although renders are the normal expected part of how React works,
it's also true that that render work can be "wasted" effort at times.
If a component's render output didn't change, and that part of the
DOM didn't need to be updated, then the work of rendering that
component was really kind of a waste of time.
React component render output should always be entirely based on
current props and current component state. Therefore, if we know
ahead of time that a component's props and state haven't changed, we
should also know that the render output would be the same, that no
changes are necessary for this component, and that we can safely skip
the work of rendering it.
When trying to improve software performance in general, there are two
basic approaches: 1) do the same work faster, and 2) do less work.
Optimizing React rendering is primarily about doing less work by
skipping rendering components when appropriate.
Component Render Optimization Techniques [?]
React offers three primary APIs that allow us to potentially skip
rendering a component:
The primary method is React.memo(), a built-in "higher order
component" type. It accepts your own component type as an argument,
and returns a new wrapper component. The wrapper component's default
behavior is to check to see if any of the props have changed, and if
not, prevent a re-render. Both function components and class
components can be wrapped using React.memo(). (A custom comparison
callback may be passed in, but it really can only compare the old and
new props anyway, so the main use case for a custom compare callback
would be only comparing specific props fields instead of all of
them.)
The other options are:
* React.Component.shouldComponentUpdate: an optional class
component lifecycle method that will be called early in the
render process. If it returns false, React will skip rendering
the component. It may contain any logic you want to use to
calculate that boolean result, but the most common approach is to
check if the component's props and state have changed since last
time, and return false if they're unchanged.
* React.PureComponent: since that comparison of props and state is
the most common way to implement shouldComponentUpdate, the
PureComponent base class implements that behavior by default, and
may be used instead of Component + shouldComponentUpdate.
All of these approaches use a comparison technique called "shallow
equality". This means checking every individual field in two
different objects, and seeing if any of the contents of the objects
are a different value. In other words, obj1.a === obj2.a && obj1.b ==
= obj2.b && ......... This is typically a fast process, because ===
comparisons are very simple for the JS engine to do. So, these three
approaches do the equivalent of const shouldRender = !shallowEqual
(newProps, prevProps).
There's also a lesser-known technique as well: if a React component
returns the exact same element reference in its render output as it
did the last time, React will skip re-rendering that particular
child. There's at least a couple ways to implement this technique:
* If you include props.children in your output, that element is the
same if this component does a state update
* If you wrap some elements with useMemo(), those will stay the
same until the dependencies change
Examples:
// The `props.children` content won't re-render if we update state
function SomeProvider({ children }) {
const [counter, setCounter] = useState(0);
return (
{children}
);
}
function OptimizedParent() {
const [counter1, setCounter1] = useState(0);
const [counter2, setCounter2] = useState(0);
const memoizedElement = useMemo(() => {
// This element stays the same reference if counter 2 is updated,
// so it won't re-render unless counter 1 changes
return ;
}, [counter1]);
return (
{memoizedElement}
);
}
Conceptually, we could say that the difference between these two
approaches is:
* React.memo(): controlled by the child component
* Same-element references: controlled by the parent component
For all of these techniques, skipping rendering a component means
React will also skip rendering that entire subtree, because it's
effectively putting a stop sign up to halt the default "render
children recursively" behavior.
How New Props References Affect Render Optimizations [?]
We've already seen that by default, React re-renders all nested
components even if their props haven't changed. That also means that
passing new references as props to a child component doesn't matter,
because it will render whether or not you pass the same props. So,
something like this is totally fine:
function ParentComponent() {
const onClick = () => {
console.log('Button clicked');
};
const data = { a: 1, b: 2 };
return ;
}
Every time ParentComponent renders, it will create a new onClick
function reference and a new data object reference, then pass them as
props to NormalChildComponent. (Note that it doesn't matter whether
we're defining onClick using the function keyword or as an arrow
function - it's a new function reference either way.)
That also means there's no point in trying to optimize renders for
"host components" like a