Usually we use React's special "key" string attribute only in combination with Lists. How and why is well explained in the React docs in the sections Lists and Keys and Reconciliation - Keys.
When you read through the Reconciliation docs you can find this explanation:
When children have keys, React uses the key to match children in the original tree with children in the subsequent tree.
This doesn't really say what's happening when you change the key, but let's explore exactly that.
Demo
We create a component Item
with a useEffect
logging out when the component mounts and unmounts. We achieve this with an empty dependency array.
const Item = () => {
useEffect(() => {
console.log("Mount item");
return () => console.log("Unmount item");
}, []);
return <div />;
};
In an App
component we can use the Item
. Every time you click on the button the string passed into key
is updated.
const App = () => {
const [id, setId] = useState("123");
return (
<>
<Item key={id} />
<button onClick={() => setId(Math.random().toString())}>
update
</button>
</>
);
};
The result looks like this
That's quite interesting! By changing the key on a component we can force it to remount.
Here you can find a working CodeSandbox example and try it yourself.
Real World Use Case
How is this even relevant? My main use-case so far was to force resetting the local state of a child in the component tree.
For example my team needed to render a list of items in a sidebar. Whenever you select an item, the main content shows a form to update each item.
Initially we built it in a way that a Detail
component would have local state, which is based on the initial props. Let me illustrate this by a simplified example. Here the default value of useState
is based on the prop contact.name
.
const Detail = (props) => {
const [name, setName] = useState(props.contact.name);
return (
<form>
<input
value={name}
onChange={(evt) => setName(evt.target.value)}
/>
</form>
);
};
Further prop changes would be ignored since useState
will ignore them.
In our App component we included the Detail
component like this:
function App() {
const [contacts, setContacts] = React.useState([
{ id: "a", name: "Anna" },
{ id: "b", name: "Max" },
{ id: "c", name: "Sarah" },
]);
const [activeContactId, setActiveContactId] = React.useState(
"a"
);
const activeContact = contacts.find(
(entry) => entry.id === activeContactId
);
return (
<>
{contacts.map((contact) => (
<button
key={contact.id}
onClick={() => setActiveContactId(contact.id)}
>
{contact.name}
</button>
))}
<Detail contact={activeContact} />
</>
);
}
In here whenever a user clicks on one of the buttons, the Detail
component receives a new contact.
Sounded good until we realized the form actually never remounts.
It may seem obvious in hindsight, but initially this was our mental model: switch contact -> component remounts
. With a deadline coming up soon, no one in the team was excited about restructuring the whole state. One of my colleagues discovered, that by adding the "key" attribute based on the item's id would allow us to achieve remounting the Detail
component.
So we changed
<Detail contact={activeContact} />
to
<Detail key={activeContact.id} contact={activeContact} />
Pretty cool, since it only took this small change to achieve our desired UX.
Feel free to try out the Demo app by yourself. It's available as a CodeSandbox example.
Should You use this Technique?
Yes and no.
In general I noticed a lot of people struggle with the key attribute and why it is needed. From my understanding it was trade-off by the React team between usability and performance.
With that in mind I would try to avoid this technique and rather use useEffect in the Detail component to reset it or lift the state to a component containing the sidebar entry as well as the form.
So when when should you use? Well, sometimes there are deadlines or architectural issues that are hard to overcome and a quick win is desired. This technique is a tool in your tool belt and if it helps you to ship a better UX earlier, why not? But that doesn't mean you should design your application to leverage this technique heavily.
Of course there is also the concern that the implementation could change since it's not part of the documentation. Luckily in a Twitter thread initiated by Sebastian Markbåge (React team), he describes it as a valid use-case and Dan Abramov even mentioned they will keep it in mind for the rewrite of the React docs.
One final note: Please add a code comment next to the key
attribute explaining why it was needed and how it works. The next person not familiar with it will thank you. 🙂
Top comments (1)
Been there, had to do that, felt dirty :)
Its pretty much down to splitting state and then not doing "split state" properly. An argument for Redux (which I rarely use), or a last minute bodge, or a whole lot of thinking about state management which can be mind bending!
Useful advice if your back is to the wall...