2.9 Списки и ключи


Прежде всего давайте посмотрим как можно преобразовывать списки в JavaScript.


В примере ниже мы используем функцию map(), чтобы принять массив numbers и помножить все его значения на 10. Мы присваиваем переменной result новый массив, который возвращает метод map(), и выводим её в лог:


Код
        
  const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  const result = numbers.map((num) => num * 10);
  console.log(result);
    

Этот код логирует [10, 20, 30, 40, 50, 60, 70, 80, 90] в консоль

В React преобразование массивов в списки элементов почти идентично.




В React вы можете создавать коллекции элементов и включать их в JSX. Для этого нужно использовать фигурные скобки {}.

В примере кода ниже, мы в цикле проходимся по массиву users, используя JavaScript-функцию map(). Мы возвращаем элемент <li> для каждого элемента массива. В итоге мы присваиваем результирующий массив элементов переменной items:


Код
        
  const users = ['Вася', 'Петя', 'Максим', 'Егор'];
  const items = users.map((user) => <li>{user}</li>);
  console.log(items);
    

Далее мы заключаем весь массив React-элементов items внутри элемента <ul> и отрисовываем его в DOM:


Код
        
  ReactDOM.render(<ul>{items}</ul>, document.getElementById('root'));
    

Посмотреть в CodePen


Этот код отображает маркированный список всех пользователей.




Обычно в приложениях мы отрисовываем списки внутри какого-нибудь компонента.

Давайте отрефакторим предыдущий пример, выделив компонент, который принимает массив users и выводит несортированный список элементов:


Код
        
  const users = ['Вася', 'Петя', 'Максим', 'Егор'];

  function UserList(props){
    const users = props.users;
    const items = users.map((user) => <li>{user}</li>);
    return (<ul>{items}</ul>)
  }

  ReactDOM.render(<UserList users={users}/>, document.getElementById('root'));
    

Когда вы выполните этот код, то получите предупреждение, что для списка элементов должен быть предоставлен ключ key. «Keys» - это специальные строковые атрибуты, которые вам нужно добавить, создавая список элементов. Почему это так важно, мы обсудим в следующем разделе.

Давайте присвоим ключ key к элементам нашего списка внутри numbers.map() и исправим проблему пропущенных ключей:


Код
        
  const users = ['Вася', 'Петя', 'Максим', 'Егор'];

  function UserList(props){
    function getKey(str){
      let key = 0;
      for (let i = 0; i < str.length; i++) {
        key += str.charCodeAt(i);
      }
      return key.toString();
    }

    const users = props.users;
    const items = users.map((user) => {
      const key = getKey(user)
      return <li key={key}>{user}</li>;
    });
    return (<ul>{items}</ul>);
  }

  ReactDOM.render(<UserList users={users}/>, document.getElementById('root'));
    

Посмотреть в CodePen





Ключи помогают React идентифицировать, какой элемент был изменен, добавлен или удален. Ключи должны быть предоставлены элементам внутри массива, чтобы дать им стабильную идентифицируемость:


Код
        
  function getKey(str){
    let key = 0;
    for (let i = 0; i < str.length; i++) {
      key += str.charCodeAt(i);
    }
    return key.toString();
   }

  const users = ['Вася', 'Петя', 'Максим', 'Егор'];
  const items = users.map((user) => {
    const key = getKey(user)
    return <li key={key}>{user}</li>;
  });
    

Лучший способ выбрать ключи – использовать строку, которая уникально идентифицирует элемент списка среди его соседей. В связи с этим, в качестве ключа вам следует использовать ID из ваших данных, принятых с сервера:


Код
        
  // Данные с сервера могут иметь примерно следующий вид: [{id: 1, name: 'Вася'}, ...]
  const userList = users.map((user) =>
    <li key={user.id}>
      {user.name}
    </li>
  );
    

В крайнем случае, когда у вас нет стабильных ID для отрисовки элементов, вы можете использовать как ключ индекс элемента:


Код
        
  const userList = users.map((user, index) =>
    // Делать так можно только в том случае, если у данных нет стабильных ID
    <li key={index}>
      {user.name}
    </li>
  );
    

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




Ключи имеют смысл только в контексте окружающего массива.

К примеру, если вы выделяете компонент UserItem, вам следует поставить ключи на все элементы <UserItem />, а не на дочерние элементы <li>.

Пример неправильного использования ключей:


Код
        
  const users = [{id: 1, name: 'Вася'},
                 {id: 2, name: 'Петя'},
                 {id: 3, name: 'Максим'},
                 {id: 4, name: 'Егор'}];

  function UserItem(props){
    const user = props.user
    //Неправильно! Здесь не нужно указывать ключ:
    return (<li key={user.id}>{user.name}</li>)
  }

  function UserList(props){
    const users = props.users;
    const items = users.map((user) => {
      //Неправильно! Здесь должен быть указан ключ:
      return <UserItem user={user}/>;
    });
    return (<ul>{items}</ul>);
  }

  ReactDOM.render(<UserList users={users}/>, document.getElementById('root'));
    

Пример правильного использования ключей:


Код
        
  const users = [{id: 1, name: 'Вася'},
                 {id: 2, name: 'Петя'},
                 {id: 3, name: 'Максим'},
                 {id: 4, name: 'Егор'}];

  function UserItem(props){
    const user = props.user
    //Правильно! Здесь не нужно указывать ключ:
    return (<li>{user.name}</li>)
  }

  function UserList(props){
    const users = props.users;
    const items = users.map((user) => {
      //Правильно! Здесь должен быть указан ключ:
      return <UserItem key={user.id} user={user}/>;
    });
    return (<ul>{items}</ul>);
  }

  ReactDOM.render(<UserList users={users}/>, document.getElementById('root'));
    

Посмотреть в CodePen


Вот хорошее эмпирическое правило: элементы внутри вызова map() требуют ключей.




Ключи, связанные с определенным массивом, должны быть уникальными только в пределах своих соседей. Однако, они не должны быть уникальными глобально. Вы можете использовать те же самые ключи для двух разных массивов:


Код
        
  function Chat(props) {
    const users = props.users;

    const userList = (
      <p> Пользователи чата:
        {users.map((user) =>
          <b key={user.id}> {user.name}; </b>
        )}
      </p>
    );

    const messageList = props.messages.map((message) => {
      let author = null;

      // Находим автора сообщения в списке users
      users.forEach((user) => {if(user.id === message.authorId) author = user});

      return (
        <p key={message.id}>
          <b>{author.name}: </b>
          <span>{message.message}</span>
        </p>
      )
    });

    return (
      <p>
        {userList}
        {messageList}
      </p>
    );
  }

  const users = [
    {id: 1, name: 'Вася'},
    {id: 2, name: 'Петя'},
    {id: 3, name: 'Ваня'}
  ];

  const messages = [
    {id: 1, message: 'Всем привет!', authorId: 1},
    {id: 2, message: 'И тебе привет!', authorId: 2},
    {id: 3, message: 'Привет, Вася :)', authorId: 3}
  ];

  ReactDOM.render(<Chat users={users} messages={messages}/>, document.getElementById('root'));
    

Посмотреть в CodePen


Ключи служат подсказками библиотеке React для корректной отрисовки списка элементов. Однако они не передаются в ваши компоненты. Если вам нужно такое же значение и в вашем компоненте, передайте его напрямую как свойство с другим именем:


Код
        
  const content = messages.map((message) =>
    <Message key={message.id} id={message.id} text={message.text}/>
  );
    

В примере выше, компонент Message видит только props.id, но не props.key.




В примерах выше мы объявляли отдельную переменную items и включали ее в JSX:


Код
        
  function UserList(props){
    const users = props.users;
    const items = users.map((user) => <UserItem user={user}/>);
    return (<ul>{items}</ul>);
  }
    

JSX позволяет встраивать в фигурные скобки любое выражение. Поэтому мы можем встроить результат вызова map():


Код
        
  function UserList(props){
    const users = props.users;
    return (
      <ul>
        {users.map((user) => <UserItem user={user}/>;)}
      </ul>
    );
  }
    

Иногда такой приём приводит к более чистому коду, но злоупотреблять этим стилем не следует. Как и в JavaScript, вам решать, что лучше в плане читабельности: вынесение кода в отдельную переменную, либо вызов функции, как мы показали выше. Имейте в виду, что если тело map() имеет слишком много вложений, возможно, наступило отличное время выделить компонент.