0

I can't make it not work well if I use index as the key. The only way is if I mutate the array and use index as the key. But since the docs says not to mutate the state (the array), so if that's the case, I can't make it not work well, contrary to what the docs is stating. How can I show that it may break?

function App() {
  const [arr, setArr] = React.useState(["A","B","C"]);

  function toggleSortOrder() {
    let newArr = [...arr];
    newArr.reverse();
    console.log(JSON.stringify(newArr));
    setArr(newArr);
  }

  return (
    <div>
      <ul>
        { arr.map((e, i) => <li key={i}>{ e }</li>) }
      </ul>
      <button onClick={toggleSortOrder} >Toggle Sort Order</button>
    </div>
  )
}

ReactDOM.render(<App />, document.querySelector("#root"));
<script src="https://unpkg.com/react@16.12.0/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16.12.0/umd/react-dom.development.js" crossorigin></script>

<div id="root"></div>

I can make it break if I mutate the state, which the docs says should not be done:

function App() {
  const [arr, setArr] = React.useState(["A","B","C"]);

  function toggleSortOrder() {
    arr.reverse();
    console.log(JSON.stringify(arr));
    setArr(arr);
  }

  return (
    <div>
      <ul>
        { arr.map((e, i) => <li key={i}>{ e }</li>) }
      </ul>
      <button onClick={toggleSortOrder} >Toggle Sort Order</button>
    </div>
  )
}

ReactDOM.render(<App />, document.querySelector("#root"));
<script src="https://unpkg.com/react@16.12.0/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16.12.0/umd/react-dom.development.js" crossorigin></script>

<div id="root"></div>

But I can't even break it if I mutate the state and use index as the key, if it is a class component:

class App extends React.Component {
  state = { arr: ["A","B","C"] };

  toggleSortOrder() {
    this.state.arr.reverse();
    console.log(JSON.stringify(this.state.arr));
    this.setState({ arr: this.state.arr });
  }

  render() {
    return (
      <div>
        <ul>
          { this.state.arr.map((e, i) => <li key={i}>{ e }</li>) }
        </ul>
        <button onClick={this.toggleSortOrder.bind(this)} >Toggle Sort Order</button>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.querySelector("#root"));
<script src="https://unpkg.com/react@16.12.0/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16.12.0/umd/react-dom.development.js" crossorigin></script>

<div id="root"></div>
nonopolarity
  • 146,324
  • 131
  • 460
  • 740

2 Answers2

2

Keys are primarily about performance. The tell react which old component corresponds to which new one, which in turn helps it quickly determine the minimal set of changes needed to update the dom. See documentation on reconciliation.

So the sense in which your code "breaks" is that react thinks it needs to edit the text inside each of the <li>'s, when it was possible to rearrange them instead. You won't really notice any performance issues with your toy example, but you could construct other examples where it matters a lot more. Here's one where i'm rendering 20,000 rows, and moving one row to the end. Performance isn't great at all, but it's better if keys are used correctly instead of based on the index.

const exampleData = [];
for (let i = 0; i < 20000; i++) {
  exampleData[i] = {
    key: i,
    name: "Element " + i,
  }
}

function App() {
  const [useKeysWell, setUseKeysWell] = React.useState(false);
  const [data, setData] = React.useState(exampleData);

  function move() {
    setData(prev => {
      let newData = [...prev.slice(1), prev[0]];
      return newData;
    });
  }

  const before = Date.now();
  setTimeout(() => {
    console.log('reconciling and committing', Date.now() - before, 'ms (approx)');
  }, 0)

  return (
    <div>
      <button onClick={move}>Move one</button>
      <button onClick={() => setUseKeysWell(prev => !prev)}>Use keys {useKeysWell ? "worse" : "better"} </button>
      <ul key={useKeysWell}>
        { data.map((e, i) => <li key={useKeysWell ? e.key : i}>{ e.name }</li>) }
      </ul>
    </div>
  )
}

ReactDOM.render(<App />, document.querySelector("#root"));
<script src="https://unpkg.com/react@16.12.0/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16.12.0/umd/react-dom.development.js" crossorigin></script>

<div id="root"></div>

With more complicated code, you could also cause problems if you have components with lifecycle hooks that you are expecting to run, but due to the incorrect keys the components are not being mounted/unmounted/updated the way you expect.

Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98
  • correct, the example above is based on performance and when applied to very large tables performance does matter – AGE Feb 18 '20 at 15:39
  • For this simple example, yes it works. But if your child components have state or have code that depends on components mounting/unmounting/updating, you may run into additional problems beyond just performance. – Nicholas Tower Feb 18 '20 at 15:39
  • you mean it can be made to show incorrect data if each row has some component? – nonopolarity Feb 18 '20 at 15:54
  • Actually, it seems like the bigger concern or "more important than a slower page" is showing incorrect data. On the ReactJS reconsiliation page, it stated "Here is an example of the issues that can be caused by using indexes as keys on CodePen" I couldn't make it not work well until later I found out I have to type something into the text box, and click "Add to end of list", for 3 times, and reorder the list. That example would show the entered text not reordering, while the "fix" as a second example would show the entered text reordered. Those 2 examples didn't say how to make it "not work." – nonopolarity Feb 18 '20 at 23:58
0

The reality is that using the index of the map function will work, it is not recommended because it is an anti-pattern:

If the array changes via add or removing an element, or modifying the number of elements in the array, React will assume that the DOM element represents the same component as before. Meaning the impact of using index could be unpredictable, such as two array items containing the same id.

It is therefore recommended that:

  • If your array elements contain a unique id, or you can form a unique id based on the structure, do so.

  • Use a global index such as a counter to help create dynamic ids... for example:

arr.map(elem => {
    counter++;
    return (
        <li key={elem + counter}>{ elem }</li>);
    );
});

This way you ensure the key has a unique identifier.

AGE
  • 3,752
  • 3
  • 38
  • 60
  • you can guarantee it unique this way? what if `arr[0] is "1"` and adding makes it `11` and `arr[10] is ""` and adding makes it `11`? – nonopolarity Feb 18 '20 at 15:49
  • you need to consider your data structure (your array and its contents) to ensure that the id's will always be unique, otherwise create a convention via a global variable which beyond counting will always create a unique id to use as a `key`. You can imagine that depending on the data structure of the array, the implementation can be trivial or more involved. As such, it is always best when re-considering your array to contain both a `value` and an `id` every time instead of a simple string and that would provide the most optimal solution, as long as you do not need to do this on the fly. – AGE Feb 18 '20 at 16:43
  • I am saying your line `
  • { elem }
  • );` doesn't work – nonopolarity Feb 18 '20 at 23:49
  • wait a second, isn't using `counter` like this almost the same as using `index`? – nonopolarity Feb 24 '20 at 18:15