В ReactJS использование индекса массива в качестве ключа считается неэффективным, но я не могу заставить его работать неправильно?

Я не могу заставить его работать неправильно, если я использую индекс в качестве ключа. Единственный способ - это изменить массив и использовать индекс в качестве ключа. Но поскольку в документах говорится, что не следует изменять состояние (массив), поэтому, если это так, я не могу заставить его работать неправильно, вопреки тому, что говорится в документах. Как я могу показать, что он может сломаться?

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>

Я могу заставить его сломаться, если изменю состояние, чего в документации делать не следует:

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>

Но я даже не могу его сломать, если изменю состояние и использую индекс в качестве ключа, если это компонент класса:

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>

2 ответа

Ключи в первую очередь о производительности. Tell реагирует, какой старый компонент соответствует какому новому, что, в свою очередь, помогает ему быстро определить минимальный набор изменений, необходимых для обновления dom. См. Документацию по сверки.

Таким образом, ваш код "ломается" в том смысле, что react думает, что ему нужно отредактировать текст внутри каждого из <li>, когда можно было их переставить. Вы действительно не заметите никаких проблем с производительностью с вашим примером игрушки, но вы можете создать другие примеры, где это имеет гораздо большее значение. Вот тот, где я визуализирую 20000 строк и перемещаю одну строку в конец. Производительность совсем не велика, но лучше, если ключи используются правильно, а не на основе индекса.

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>

С более сложным кодом вы также можете вызвать проблемы, если у вас есть компоненты с хуками жизненного цикла, которые вы ожидаете запустить, но из-за неправильных ключей компоненты не монтируются / размонтируются / обновляются не так, как вы ожидаете.

Реальность такова, что с помощью индекса mapфункция будет работать, это не рекомендуется, потому что это анти-шаблон:

Если массив изменяется путем добавления или удаления элемента или изменения количества элементов в массиве, React будет считать, что элемент DOM представляет тот же компонент, что и раньше. Это означает, что влияние использования индекса может быть непредсказуемым, например, два элемента массива содержат один и тот же идентификатор.

Поэтому рекомендуется:

  • Если элементы вашего массива содержат уникальный идентификатор или вы можете сформировать уникальный идентификатор на основе структуры, сделайте это.

  • Используйте глобальный индекс, такой как счетчик, для создания динамических идентификаторов... например:

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

Таким образом вы обеспечите key имеет уникальный идентификатор.

Другие вопросы по тегам