React JS 18 change in Component Types and Reconciliation ?
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 <MyFunctionComponent />
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 <div>
to <span>
or <ComponentA>
to <ComponentB>
, 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:
---
function ParentComponent() {
// This creates a new `ChildComponent` reference every time!
function ChildComponent() {}
return <ChildComponent />
}
// Instead, always define components separately:
// This only creates one component type reference
function ChildComponent() {}
function ParentComponent() {
return <ChildComponent />
}
---
Keys and Reconciliation
The other way that React identifies component "instances" is via the key
pseudo-prop. key
is an instruction to React, and will never be passed through to the actual component. React treats key
as a unique identifier that it can use to differentiate specific instances of a component type.
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!
Here's an example of why this matters. Say I render a list of 10 <TodoListItem>
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 <TodoListItem key={6}>
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 <DetailForm key={selectedItem.id}>
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.