React - Fun with keys

in utopian-io •  6 months ago

React uses the key attribute during its reconciliation phase to decide which elements can be reused for the next render.
They are important for dynamic lists. React will compare the keys of the new element with the previous keys and 1) mount components having a new key 2) unmount components whose keys are not used anymore.
Many React developers heard of the general advice that you should not use index as a key, but what exactly can go wrong when using keys in a bad way? What else can we do when we play around with keys?

For better understanding, let's consider the example of rendering a list of inputs.
When clicking a button, we will insert a new item with text Front to the front of the list.

import React from "react";
import { render } from "react-dom";

class Item extends React.PureComponent {
  state = {
    text: this.props.text
  };

  onChange = event => {
    this.setState({
      text: event.target.value
    });
  };

  componentDidMount() {
    console.log("Mounted ", this.props.text);
  }

  componentWillUnmount() {
    console.log("Unmounting ", this.props.text);
  }

  render() {
    console.log("rerendering ", this.props.text);
    const { text } = this.state;
    return (
      <li>
        <input value={text} onChange={this.onChange} />
      </li>
    );
  }
}

class App extends React.Component {
  state = {
    items: [
      {
        text: "First",
        id: 1
      },
      {
        text: "Second",
        id: 2
      }
    ]
  };

  addItem = () => {
    const items = [{ text: "Front", id: Date.now() }, ...this.state.items];
    this.setState({ items });
  };

  render() {
    return (
      <div>
        <ul>
          {this.state.items.map((item, index) => (
            <Item {...item} key={index} />
          ))}
        </ul>
        <button onClick={this.addItem}>Add Item</button>
      </div>
    );
  }
}

render(<App />, document.getElementById("root"));

Using index as a key the following happens:

React Keys Index

Another Item with text Second instead of Front is inserted at the back of the list?
Here's what happens:

  1. Item is an uncontrolled component: The text the user writes into its input field is stored as state
  2. A new data item { text: "Front" } is inserted to the beginning of the list data.
  3. The list is re-rendered with the index value as key. So the previous components are re-used for the first two data items and given the correct props Front and First, but the state is not updated in Item. That's why the first two component instances keep the same text.
  4. A new component instance is created for key: 2 because no previous matching key is found. It is filled with the props of the last list data item which is Second.

React Keys Index Explanation

Another interesting point is the render calls that happen. Item is a PureComponent, so it only updates when the text prop (or state) changes:

rerendering  Front
rerendering  First
rerendering  Second
Mounted  Second

All components are re-rendered. This happens because the element with key: 0 is reused for the first data item and receives its props, but the first data item is now the new Front object, triggering a render. The same happens with the other components because the old data items are now all shifted by one place.

So what's the fix?
The fix is easy, we give each list data item a unique id once upon creation (not on each render!).
All components instances will be matched with their corresponding data item, i.e., they receive the same props as before - avoiding another render.

Ignoring the performance benefits when using ids in dynamic lists for now, the example shows that bugs introduced by keys only ever happen with regards to uncontrolled components, components that keep internal state.

If we rewrite Item as a controlled component, by moving the state out of it, the bug is gone.

Why? Again, because the bug was reusing a component for a different data item, and therefore the internal state still reflected the state of the previous data item, but the props of a different one.
Making the component controlled, by removing its state completely, we, of course, don't have this discrepancy anymore. (But there's still the issue with the unnecessary re-renders.)

Abusing key to fix broken third-party components

React only needs keys when matching several elements, so setting a key on a single child is not needed.
But it can still be useful to set a key on a single child component.
If you change the key, React will throw away the whole component (unmount it), and mount a new component instance in its place.
Why could this be useful?

Again, we're coming back to uncontrolled components. Sometimes, you're using a third-party component and you cannot modify its code to make it controlled.
If a component has some internal state and it's implemented in a bad way, i.e., the state is derived only once in the constructor, but getDerivedStateFromProps / componentWillReceiveProps is not implemented to reflect reoccurring props changes in its internal state, the standard React toolbox cannot help you here. There is no forceRemount.
However, we can just set a new key on this component to achieve the desired behavior of completely initializing a new component. The old component will be unmounted, and a new one is mounted with the new props initializing the state.

TL;DR:

Using index as a key can:

  1. lead to unnecessary re-renders
  2. introduce bugs when the list items are uncontrolled components but still use props

The key property can be used to force a complete remount of a component which can sometimes be useful.


Originally published at https://cmichel.io

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Thanks for the article. I wondered why using the index as a key is considered bad practice. Now I know, mission accomplished :)