0% found this document useful (0 votes)
8 views35 pages

Advanced React c6

Chapter 6 explores React's diffing and reconciliation algorithms, detailing how React determines which components to re-render, add, or remove based on changes in state. It discusses the importance of component structure, particularly avoiding the creation of components inside other components to prevent performance issues and unexpected behavior. The chapter also highlights the use of the 'key' attribute for managing lists and the implications of conditional rendering on component state.

Uploaded by

Bora
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8 views35 pages

Advanced React c6

Chapter 6 explores React's diffing and reconciliation algorithms, detailing how React determines which components to re-render, add, or remove based on changes in state. It discusses the importance of component structure, particularly avoiding the creation of components inside other components to prevent performance issues and unexpected behavior. The chapter also highlights the use of the 'key' attribute for managing lists and the implications of conditional rendering on component state.

Uploaded by

Bora
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Chapter 6.

Deep dive into diffing


and reconciliation

In the previous chapters, we covered the basics of React's reconciliation


and diffing algorithms. We now know that when we create React
Elements, such as const a = <Child /> , we're actually creating
objects. The HTML-like syntax ( JSX ) is just syntax sugar that is
transformed into the React.createElement function. That function
returns a description object with the type property that points to
either a component, a memoized component, or a string with an HTML
tag.

Also, we know that if the reference to that object itself changes between
re-renders, then React will re-render this Element if its type remains
the same and the component in type is not memoized with
React.memo .

But this is just the beginning. There are more variables and moving
pieces here, and understanding this process in detail is very important. It
will allow us to fix some very not-obvious bugs, implement the most
performant lists, reset the state when we need it, and avoid one of the
biggest performance killers in React. All in one go. None of it seems
connected at first glance, but all of this is part of the same story: how
React determines which components need to be re-rendered, which
components need to be removed, and which ones need to be added to the
screen.

In this chapter, we'll investigate a few very curious bugs, dive very deep
into how things work under the hood, and in the process of doing so, we
will learn:

Page 91
How React's Diffing and Reconciliation algorithm works.
What happens when a state update is triggered and React needs to
re-render components.
Why we shouldn't create components inside other components.
How to solve the bug of two different components sharing the
same state.
How React renders arrays and how we can influence that.
What is the purpose of the "key" attribute.
How to write the most performant lists possible.
Why we would use the "key" attribute outside of dynamic lists.

The Mysterious Bug


Let's start with a little mystery to keep things interesting.

Imagine you render a component conditionally. If "something" is true ,


show me this component. Otherwise, show me something else. For
example, I'm developing a "sign up" form for my website, and part of
that form is whether those who sign up are a company or just a regular
human fellow, for some crazy tax purposes. So I want to show the
"Company Tax ID" input field only after the user clicks the "yes, I'm
signing up as a company" checkbox. And for people, show a text "You
don't have to give us your tax ID, lucky human."

The code for this app will look something like this:

const Form = () => {


const [isCompany, setIsCompany] = useState(false);

return (
<>
{/* checkbox somewhere here */}
{isCompany ? (
<Input id="company-tax-id-number" placeholder="Enter you
company ID" ... />

Page 92
) : (
<TextPlaceholder />
)}
</>
)
}

What will happen here from a re-rendering and mounting perspective if


the user actually claims that they are a company and the value
isCompany changes from the default false to true ?

No surprises here, and the answer is pretty intuitive: the Form


component will re-render itself, the TextPlaceholder component
will be unmounted, and the Input component will be mounted. If I flip
the checkbox back, the Input will be unmounted again, and the
TextPlaceholder will be mounted.

From a behavioral perspective, all of this means that if I type something


in the Input , flip the checkbox, and then flip it back, whatever I typed
there will be lost. Input has its own internal state to hold the text,
which will be destroyed when it unmounts and will be re-created from
scratch when it mounts back.

But what will happen if I actually need to collect the tax ID from people
as well? And the field should look and behave exactly the same, but it
will have a different id , different onChange callback, and other
different settings. Naturally, I'd do something like this:

const Form = () => {


const [isCompany, setIsCompany] = useState(false);

return (
<>
{/* checkbox somewhere here */}
{isCompany ? (
<Input id="company-tax-id-number" placeholder="Enter you
company Tax ID" ... />

Page 93
) : (
<Input id="person-tax-id-number" placeholder="Enter you
personal Tax ID" ... />
)}
</>
)
}

What will happen here now?

The answer is, of course, again pretty intuitive and exactly as any
sensible person would expect... The unmounting doesn't happen
anymore! If I type something in the field and then flip the checkbox, the
text is still there! React thinks that both of those inputs are actually the
same thing, and instead of unmounting the first one and mounting the
second one, it just re-renders the first one with the new data from the
second one.

Interactive example and full code


https://advanced-react.com/examples/06/01

If you're not surprised by this at all and can without hesitation say, "Ah,
yeah, it's because of [the reason]," then wow, can I get your autograph?
For the rest of us who got an eye twitch and a mild headache because of
this behavior, it's time to dive into React's reconciliation process to get
the answer.

Diffing and Reconciliation


It's all because of the DOM[5]. Or to be precise, the fact that we don't
have to deal with it directly when we're writing React code. This is very
convenient for us: instead of doing appendChild or comparing
attributes manually, we just write components. And then React

Page 94
transforms whatever we give to it into DOM elements on the screen with
appropriate data. When we write code like this:

const Input = ({ placeholder }) => {


return (
<input
type="text"
id="input-id"
placeholder={placeholder}
/>
);
};

// somewhere else
<Input placeholder="Input something here" />;

we expect React to add the normal HTML input tag with


placeholder set in the appropriate place in the DOM structure. If we
change the placeholder value in the React component, we expect
React to update our DOM element with the new value and to see that
value on the screen. Ideally, instantly. So, React can't just remove the
previous input and append a new one with the new data. That would be
terribly slow. Instead, it needs to identify that already existing input
DOM element and just update its attributes. If we didn't have React,
we'd have to do something like this:

const input = document.getElementById('input-id');


input.placeholder = 'new data';

In React, we don't have to; it handles it for us. It does so by creating and
modifying what we sometimes call the "Virtual DOM." This Virtual DOM
is just a giant object with all the components that are supposed to
render, all their props, and their children - which are also objects of the
same shape. Just a tree. What the Input component from the example
above should render will be represented as something like this:

Page 95
{
type: "input", // type of element that we need to render
props: {...}, // input's props like id or placeholder
... // bunch of other internal stuff
}

If our Input component was rendering something more:

const Input = () => {


return (
<>
<label htmlFor="input-id">{label}</label>
<input type="text" id="input-id" />
</>
);
};

then label and input from React's perspective would be just an


array of those objects:

[
{
type: 'label',
... // other stuff
},
{
type: 'input',
... // other stuff
}
]

DOM elements like input or label will have their "type" as strings,
and React will know to convert them to the DOM elements directly. But
if we're rendering React components, they are not directly correlated
with DOM elements, so React needs to work around that somehow.

Page 96
const Component = () => {
return <Input />;
};

In this case, it will put the component's function as the "type." It just
grabs the entire function that we know as the Input component and
puts it there:

{
type: Input, // reference to that Input function we declared
earlier
... // other stuff
}

And then, when React gets a command to mount the app (initial render),
it iterates over that tree and does the following:

If the "type" is a string, it generates the HTML element of that


type.
If the "type" is a function (i.e., our component), it calls it and
iterates over the tree that this function returned.

Until it eventually gets the entire tree of DOM nodes that are ready to be
shown. A component like this, for example:

const Component = () => {


return (
<div>
<Input placeholder="Text1" id="1" />
<Input placeholder="Text2" id="2" />
</div>
);
};

will be represented as:

Page 97
{
type: 'div',
props: {
// children are props!
children: [
{
type: Input,
props: { id: "1", placeholder: "Text1" }
},
{
type: Input,
props: { id: "2", placeholder: "Text2" }
}
]
}
}

Which will on mounting resolve into HTML like this:

<div>
<input placeholder="Text1" id="1" />
<input placeholder="Text2" id="2" />
</div>

Finally, when everything is ready, React appends those DOM elements to


the actual document with JavaScript's appendChild [6] command.

Reconciliation and state update


After that, the fun begins. Suppose one of the components from that tree
has state, and its update was triggered (re-render is triggered). React
needs to update all the elements on the screen with the new data that
comes from that state update. Or maybe add or remove some new
elements.

Page 98
So it begins its journey through that tree again, starting from where the
state update was initiated. If we have this code:

const Component = () => {


// return just one element
return <Input />;
};

React will understand that the Component returns this object when
rendered:

{
type: Input,
... // other internal stuff
}

It will compare the "type" field of that object from "before" and "after"
the state update. If the type is the same, the Input component will be
marked as "needs update," and its re-render will be triggered. If the type
has changed, then React, during the re-render cycle, will remove
(unmount) the "previous" component and add (mount) the "next"
component. In our case, the "type" will be the same since it's just a
reference to a function, and that reference hasn't changed.

If I were doing something conditional with that Input , like returning


another component:

const Component = () => {


if (isCompany) return <Input />;

return <TextPlaceholder />;


};

then, assuming that the update was triggered by isCompany value


flipping from true to false , the objects that React will be comparing

Page 99
are:

// Before update, isCompany was "true"


{
type: Input,
...
}

// After update, isCompany is "false"


{
type: TextPlaceholder,
...
}

You guessed the result, right? "Type" has changed from Input to
TextPlaceholder references, so React will unmount Input and
remove everything associated with it from the DOM. And it will mount
the new TextPlaceholder component and append it to the DOM for
the first time. Everything that was associated with the Input field,
including its state and everything you typed there, is destroyed.

Why we can't define components


inside other components
Now that this behavior is clear, we can answer one very important
question: why shouldn't we create components inside other
components? Why is code like this usually considered an anti-pattern?

const Component = () => {


const Input = () => <input />;

return <Input />;


};

Page 100
If we look at this code from the reconciliation and definition object
perspective, this is what our Component returns:

{
type: Input,
}

It's just an object that has a "type" property that points to a function.
However, the function is created inside Component . It's local to it and
will be recreated with every re-render as a result. So when React tries to
compare those types, it will compare two different functions: one before
re-render and one after re-render. And as we know from Chapter 5.
Memoization with useMemo, useCallback and React.memo, we can't
compare functions in JavaScript, not like this.

const a = () => {};


const b = () => {};

a === b; // will always be false

As a result, the "type" of that child will be different with every re-render,
so React will remove the "previous" component and mount the "next"
one.

If the component is heavy enough, we will even see a "flickering" effect


on the screen: it will briefly disappear and then re-appear back. This is
what we usually call re-mounting. And normally it's not intentional and
it's terrible for performance - re-mounting will take at least twice as long
as a normal re-render. In addition, since the "before" component and
everything that is associated with it is destroyed, we'll see quite curious
and hard-to-trace bugs as a result. If that component is supposed to hold
state or focus, for example, those will disappear on every re-render.

Interactive example and full code

Page 101
https://advanced-react.com/examples/06/02

In the associated code example above, you can see how it behaves: the
input component triggers a re-render with every keystroke, and the
"ComponentWithState" is re-mounted. As a result, if you click on that
component to change its state to "active" and then start typing, that state
will disappear.

Declaring components inside other components like this can be one of


the biggest performance killers in React.

The answer to the mystery


Now let's return to the mysterious code from the beginning and solve
that bug:

const Form = () => {


const [isCompany, setIsCompany] = useState(false);

return (
<>
{/*checkbox somewhere here*/}
{isCompany ? (
<Input id="company-tax-id-number" placeholder="Enter you
company Tax ID" ... />
) : (
<Input id="person-tax-id-number" placeholder="Enter you
personal Tax ID" ... />
)}
</>
)
}

Page 102
If the isCompany variable changes from true to false here, which
objects will be compared?

Before, isCompany is true :

{
type: Input,
... // the rest of the stuff, including props like id="company-
tax-id-number"
}

After, isCompany is false :

{
type: Input,
... // the rest of the stuff, including props like id="person-
tax-id-number"
}

From the React perspective, the "type" hasn't changed. Both of them
have a reference to exactly the same function: the Input component.
The only thing that has changed, thinks React, are the props: id
changed from "company-tax-id-number" to "person-tax-id-
number" , placeholder changed, and so on.

So, in this case, React does what it was taught: it simply takes the
existing Input component and updates it with the new data. I.e., re-
renders it. Everything that is associated with the existing Input , like its
DOM element or state, is still there. Nothing is destroyed. This results in
the behavior that we've seen: I type something in the input, flip the
checkbox, and the text is still there.

This behavior isn't necessarily bad. I can see a situation where re-
rendering instead of re-mounting is exactly what I would want. But in
this case, I'd probably want to fix it and ensure that inputs are reset and

Page 103
re-mounted every time I switch between them: they are different entities
from the business logic perspective, so I don't want to re-use them.

There are at least two easy ways to fix it: arrays and keys.

Reconciliation and arrays


Until now, I've only mentioned the fact of arrays in that data tree. But it's
highly unlikely that anyone can write a React app where every single
component returns only one element. We need to talk about arrays of
elements and how they behave during re-renders in more detail now.
Even our simple Form actually has an array:

const Form = () => {


const [isCompany, setIsCompany] = useState(false);

return (
<>
{/*checkbox somewhere here*/}
{isCompany ? (
<Input id="company-tax-id-number" ... />
) : (
<Input id="person-tax-id-number" ... />
)}
</>
)
}

It returns a Fragment (that thing: <>...</> ) that has an array of


children: there is a checkbox hidden there. The actual code is more like
this:

const Form = () => {


const [isCompany, setIsCompany] = useState(false);

Page 104
return (
<>
<Checkbox onChange={() => setIsCompany(!isCompany)} />
{isCompany ? (
<Input id="company-tax-id-number" ... />
) : (
<Input id="person-tax-id-number" ... />
)}
</>
)
}

During re-render, when React sees an array of children instead of an


individual item, it just iterates over it and then compares "before" and
"after" elements and their "type" according to their position in the array.

Basically, if I flip the checkbox and trigger the Form re-render, React
will see this array of items:

[
{
type: Checkbox,
},
{
type: Input, // our conditional input
},
];

and will go through them one by one. First element. "Type" before:
Checkbox , "type" after: also Checkbox . Re-use it and re-render it.
Second element. Same procedure. And so on.

Even if some of those elements are rendered conditionally like this:

isCompany ? <Input /> : null;

Page 105
React will still have a stable number of items in that array. Just
sometimes, those items will be null . If I re-write the Form like this:

const Form = () => {


const [isCompany, setIsCompany] = useState(false);

return (
<>
<Checkbox onChange={() => setIsCompany(!isCompany)} />
{isCompany ? <Input id="company-tax-id-number" ... /> : null}
{!isCompany ? <Input id="person-tax-id-number" ... /> : null}
</>
)
}

it will be an array of always three items: Checkbox , Input or null ,


and Input or null .

So, what will happen here when the state changes and re-render runs
throughout the form?

Before, isCompany is false :

[{ type: Checkbox }, null, { type: Input }];

After, isCompany is true :

[{ type: Checkbox }, { type: Input }, null];

And when React starts comparing them, item by item, it will be:

the first item, Checkbox before and after → re-render


Checkbox
the second item, null before and Input after → mount
Input
third item, Input before, null after → unmount Input

Page 106
And voila! Magically, by changing the inputs' position in the render
output, without changing anything else in the logic, the bug is fixed, and
inputs behave exactly as I would expect!

Interactive example and full code


https://advanced-react.com/examples/06/03

Reconciliation and "key"


There is another way to fix the same bug: with the help of the "key"
attribute.

The "key" should be familiar to anyone who has written any lists in
React. React forces us to add it when we iterate over arrays of data:

const data = ['1', '2'];

const Component = () => {


// "key" is mandatory here!
return data.map((value) => <Input key={value} />);
};

The output of this component should be clear by now: just an array of


objects with the "type" Input :

[
{ type: Input }, // "1" data item
{ type: Input }, // "2" data item
];

But the problem with dynamic lists like this is that they are, well,
dynamic. We can re-order them, add new items at the beginning or end,
and generally mess around with them.

Page 107
Now, React faces an interesting task: all components in that array are of
the same type. How to detect which one is which? If the order of those
items changes:

[
{ type: Input }, // "2" data item now, but React doesn't know
that
{ type: Input }, // "1" data item now, but React doesn't know
that
];

how to make sure that the correct existing element is re-used? Because if
it just relies on the order of elements in that array, it will re-use the
instance of the first element for the data of the second element, and vice
versa. This will result in weird behavior if those items have state: it will
stay with the first item. If you type something in the first input field and
re-order the array, the typed text will remain in the first input.

This is why we need "key": it's basically React's version of a unique


identifier of an element within children's array that is used between re-
renders. If an element has a "key" in parallel with "type," then during re-
render, React will re-use the existing elements, with all their associated

Page 108
state and DOM, if the "key" and "type" match "before" and "after."
Regardless of their position in the array.

With this array, the data would look like this. Before re-ordering:

[
{ type: Input, key: '1' }, // "1" data item
{ type: Input, key: '2' }, // "2" data item
];

After re-ordering:

[
{ type: Input, key: '2' }, // "2" data item, React knows that
because of "key"
{ type: Input, key: '1' }, // "1" data item, React knows that
because of "key"
];

Now, with the key present, React will know that after re-render, it needs
to re-use an already created element that used to be in the first position.
So it will just swap input DOM nodes around. And the text that we
typed in the first element will move with it to the second position.

Page 109
Interactive example and full code
https://advanced-react.com/examples/06/04

"Key" attribute and memoized list


One of the most common misconceptions about the key attribute and
lists is that key is needed there for performance reasons. That adding
key to the dynamic array prevents array items from re-rendering. As
you can see from the above, that's not how the key works. Key helps
React to identify which already existing instance it should re-use when it
re-renders those items. Re-render will still happen, like with any
components rendered inside another component.

If we want to prevent re-renders of items, we need to use React.memo


for that. For static arrays (those that don't change their elements or their
position), it's very easy: just wrap whatever the item element is in
React.memo and use either some sort of id property of that item or
just the array's index in the key . Anything that is stable between re-
renders will do.

const data = [
{ id: 'business', placeholder: 'Business Tax' },
{ id: 'person', placeholder: 'Person Tax' },
];
const InputMemo = React.memo(Input);
const Component = () => {
// array's index is fine here, the array is static
return data.map((value, index) => (
<InputMemo
key={index}
placeholder={value.placeholder}
/>
));
};

Page 110
If the re-render of the Parent is triggered, none of the InputMemo
components will re-render: they are wrapped in React.memo , and the
key for any of the items hasn't changed.

With dynamic arrays, it's a bit more interesting, and this is where the
key plays a crucial role. What will happen here if what triggered the re-
render is the re-ordering of that array?

// array before re-render


[
{ id: 'business', placeholder: 'Business Tax' },
{ id: 'person', placeholder: 'Person Tax' },
]

// array after re-render


[
{ id: 'person', placeholder: 'Person Tax' },
{ id: 'business', placeholder: 'Business Tax' },
]

If we just use the array's index as a key again, then from React's
perspective, the item with the key="0" will be the first item in the
array before and after the re-render. But the prop placeholder will
change: it will transition from "Business Tax" to "Person Tax." As a
result, even if this item is memoized, from React's perspective, the prop
on it changed, so it will re-render it as if memoization doesn't exist!

Page 111
The fix for this is simple: we need to make sure that the key matches
the item it identifies. In our case, we can just put the id there:

const Parent = () => {


// if array can be sorted, or number of its items can change,
then "index" as "key" is not a good idea
// we need to use something that identifies an array item instead
return sortedData.map((value, index) => (
<InputMemo
key={value.id}
placeholder={value.placeholder}
/>
));
};

If the data has nothing unique like an id , then we'd need to iterate over
that array somewhere outside of the component that re-renders and add
that id there manually.

In the case of our inputs, if we use the id for key , the item with the
key="business" will still have the prop placeholder="Business

Page 112
Tax," just in a different place in the array. So React will just swap the
associated DOM nodes around, but the actual component won't re-
render.

And exactly the same story happens if we were adding another input at
the beginning of the array. If we use the array's index as key , then
the item with the key="0" , from React's perspective, will just change
its placeholder prop from "Business Tax" to "New tax"; key="1"
item will transition from "Person Tax" to "Business Tax". So they both
will re-render. And the new item with the key="2" and the text
"Person Tax" will be mounted from scratch.

Page 113
And if we use the id as a key instead, then both "Business Tax" and
"Person Tax" will keep their keys, and since they are memoized, they
won't re-render. And the new item, with the key "New tax", will be
mounted from scratch.

Page 114
Interactive example and full code
https://advanced-react.com/examples/06/05

State reset technique


Why does all of this logic with keys matter for our Form component
and its bug from the very beginning of the chapter? Fun fact: "key" is just
an attribute of an element, it's not limited to dynamic arrays. In any
children's array, it will behave exactly the same way. And as we already
found out, the object definition of our Form with the bug from the very
beginning:

const Form = () => {


const [isCompany, setIsCompany] = useState(false);

Page 115
return (
<>
<Checkbox onChange={() => setIsCompany(!isCompany)} />
{isCompany ? (
<Input id="company-tax-id-number" ... />
) : (
<Input id="person-tax-id-number" ... />
)}
</>
)
}

has an array of children:

[
{ type: Checkbox },
{ type: Input }, // react thinks it's the same input between re-
renders
];

All we need to fix the initial bug is to make React realize that those
Input components between re-renders are actually different
components and should not be re-used. If we add a "key" to those inputs,
we'll achieve exactly that.

{isCompany ? (
<Input id="company-tax-id-number" key="company-tax-id-number" ...
/>
) : (
<Input id="person-tax-id-number" key="person-tax-id-number" ...
/>
)}

Now, the array of children before and after re-render will change.

Before, isCompany is false :

Page 116
[
{ type: Checkbox },
{
type: Input,
key: 'person-tax-id-number',
},
];

After, isCompany is true :

[
{ type: Checkbox },
{
type: Input,
key: 'company-tax-id-number',
},
];

Voila, the keys are different! React will drop the first Input and mount
from scratch the second one. State is now reset to empty when we switch
between inputs.

Interactive example and full code


https://advanced-react.com/examples/06/06

This technique is known as "state reset". It has nothing to do with state


per se, but it's sometimes used when there is a need to reset the state of
an uncontrolled component (like an input field) to a default value. You
don't even have to have two components for this, like I had above. One
will do. Any unique value in key that changes depending on your
conditions will work for this. If you want to force state reset on URL
change, for example, it could be as simple as this:

const Component = () => {

Page 117
// grab the current url from our router solution
const { url } = useRouter();

// I want to reset that input field when the page URL changes
return <Input id="some-id" key={url} />;
};

But be careful here, though. It's not just "state reset" as you can see. It
forces React to unmount a component completely and mount a new one
from scratch. For big components, that might cause performance
problems. The fact that the state is reset is just a by-product of this total
destruction.

Using "key" to force reuse of an


existing element
Another fun fact: if we actually needed to reuse an existing element,
"key" could help with that as well. Remember this code, where we fixed
the bug by rendering the Input element in different positions in the
children array?

const Form = () => {


const [isCompany, setIsCompany] = useState(false);

return (
<>
<Checkbox onChange={() => setIsCompany(!isCompany)} />
{isCompany ? <Input id="company-tax-id-number" ... /> : null}
{!isCompany ? <Input id="person-tax-id-number" ... /> : null}
</>
)
}

Page 118
When the isCompany state variable changes, Input components will
unmount and mount since they are in different positions in the array.
But! If I add the "key" attribute to both of those inputs with the same
value, the magic happens.

<>
<Checkbox onChange={() => setIsCompany(!isCompany)} />
{isCompany ? <Input id="company-tax-id-number" key="tax-input"
... /> : null}
{!isCompany ? <Input id="person-tax-id-number" key="tax-input"
... /> : null}
</>

From the data and re-renders' perspective, it will now be like this.

Before, isCompany is false :

[
{ type: Checkbox },
null,
{
type: Input,
key: 'tax-input',
},
];

After, isCompany is true :

[
{ type: Checkbox },
{ type: Input, key: "tax-input" }
null
]

React sees an array of children and sees that before and after re-renders,
there is an element with the Input type and the same "key." So it will

Page 119
think that the Input component just changed its position in the array
and will re-use the already created instance for it. If we type something,
the state is preserved even though the Input s are technically different.

Interactive example and full code


https://advanced-react.com/examples/06/07

For this particular example, it's just a curious behavior, of course, and
not very useful in practice. But I could imagine it being used for fine-
tuning the performance of components like accordions, tabs content, or
some galleries.

Why we don't need keys outside of


arrays?
Let's have a bit more fun with reconciliation, now that the mystery is
solved and the algorithm is more or less clear. There are still a few mini-
questions and mysteries there. For example, have you noticed that React
never forced you to add keys to anything unless you're iterating over an
array?

The object definition of this:

const data = ['1', '2'];

const Component = () => {


// "key" is mandatory here!
return (
<>
{data.map((value) => (
<Input key={value} />
))}
</>

Page 120
);
};

and this:

const Component = () => {


// no-one cares about "key" here
return (
<>
<Input />
<Input />
</>
);
};

will be exactly the same, just a fragment with two inputs as a children
array:

[{ type: Input }, { type: Input }];

So why, in one case, do we need a "key" for React to behave, and in


another - don't?

The difference is that the first example is a dynamic array. React doesn't
know what you will do with this array during the next re-render: remove,
add, or rearrange items, or maybe leave them as-is. So it forces you to
add the "key" as a precautionary measure, in case you're messing with
the array on the fly.

Where is the fun here, you might ask? Here it is: try to render those
inputs that are not in an array with the same "key," applied
conditionally:

const Component = () => {


const [isReverse, setIsReverse] = useState(false);
// no-one cares about "key" here

Page 121
return (
<>
<Input key={isReverse ? 'some-key' : null} />
<Input key={!isReverse ? 'some-key' : null} />
</>
);
};

Try to predict what will happen if I type something in those inputs and
toggle the boolean on and off.

Interactive example and full code


https://advanced-react.com/examples/06/08

Dynamic arrays and normal


elements together
If you've read the entire chapter carefully, there's a possibility that by
now you might have had a minor heart attack. I certainly had one when I
was investigating all of this. Because…

If dynamic items are transformed into an array of children that is


no different than normal elements stuck together,
and if I put normal items after a dynamic array,
and add or remove an item in the array,

does it mean that items after this array will always re-mount
themselves?? Basically, is this code a performance disaster or not?

const data = ['1', '2'];

const Component = () => {


return (

Page 122
<>
{data.map((i) => (
<Input key={i} id={i} />
))}
{/* will this input re-mount if I add a new item in the
array above? */}
<Input id="3" />
</>
);
};

Because if this is transformed into an array of three children - the first


two are dynamic, and the last one static - it will be. If this is the case,
then the definition object will be this:

[
{ type: Input, key: 1 }, // input from the array
{ type: Input, key: 2 }, // input from the array
{ type: Input }, // input after the array
];

And if I add another item to the data array, on the third position there
will be an Input element with the key="3" from the array, and the
"manual" input will move to the fourth position, which would mean from
the React perspective that it's a new item that needs to be mounted.

Luckily, this is not the case. Phew... React is smarter than that.

When we mix dynamic and static elements, like in the code above, React
simply creates an array of those dynamic elements and makes that entire
array the very first child in the children's array. This is going to be the
definition object for that code:

[
// the entire dynamic array is the first position in the
children's array

Page 123
[
{ type: Input, key: 1 },
{ type: Input, key: 2 },
],
{
type: Input, // this is our manual Input after the array
},
];

Our manual Input will always have the second position here. There
will be no re-mounting. No performance disaster. The heart attack was
uncalled for.

Key takeaways
Ooof, that was a long chapter! Hope you had fun with the investigation
and the mysteries and learned something cool while doing so. A few
things to remember from all of that:

React will compare elements between re-renders with elements in


the same place in the returned array on any level of hierarchy. The
first one with the first one, the second with the second, etc.
If the type of the element and its position in the array is the same,
React will re-render that element. If the type changes at that
position, then React will unmount the previous component and
mount the new one.
An array of children will always have the same number of children
(if it's not dynamic). Conditional elements ( isSomething ? <A
/> : <B /> ) will take just one place, even if one of them is
null .
If the array is dynamic, then React can't reliably identify those
elements between re-renders. So we use the key attribute to help
it. This is important when the array can change the number of its
items or their position between re-renders (re-order, add,

Page 124
remove), and especially important if those elements are wrapped
in React.memo .
We can use the key outside of dynamic arrays as well to force
React to recognize elements at the same position in the array with
the same type as different. Or to force it to recognize elements at
different positions with the same type as the same.
We can also force unmounting of a component with a key if that
key changes between re-renders based on some information (like
routing). This is sometimes called "state reset".

Page 125

You might also like