Using React's Key Attribute to remount a Component

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

Remount example with console.log
output

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.

An example demo of the outcome

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.

An example demo of the failing case

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. 🙂

Published at: 2020-04-27 Updated at: 2020-05-22


Join the Newsletter

Thoughts on Software Engineering with a focus on React & GraphQL.