До этого мы с вами работали с отдельными, статичными компонентами. Мы выводили заголовки, параграфы, кнопки. Но давайте посмотрим правде в глаза. Реальный мир веб-разработки состоит из данных и эти данные часто представлены в виде списков. Новостная лента в социальной сети, товары в интернет-магазине, сообщения в чате, пункты в навигационном меню. Всё это динамические списки.
Сегодня, на седьмом уроке, мы научимся оживлять наши приложения, наполняя их такими динамическими списками. Мы познакомимся с новым лучшим другом, методом map() и разберёмся с одним из самых важных атрибутов в React, это key (ключом). Это фундаментальная тема, понимание которой важно для создания эффективных и корректно работающих React-приложений. И я уверен, что к концу этого урока вы будете чувствовать себя с ними как рыба в воде.
Динамические списки
Давайте начнём с простого вопроса, а зачем нам всё это нужно? Почему бы просто не написать десять одинаковых компонентов <TodoItem /> подряд, если нам нужен список из десяти задач?
Представьте, что вы создаёте приложение для управления задачами (To-Do List). Пользователь может добавлять новые задачи, удалять старые, помечать их как выполненные. Количество задач постоянно меняется. Если бы мы попытались сделать это статически, наш код превратился бы в кошмар.
// ПЛОХОЙ и НЕГИБКИЙ ПОДХОД function TodoList() { return ( <ul> <TodoItem text="Выучить React" completed={false} /> <TodoItem text="Написать первый проект" completed={true} /> <TodoItem text="Устроиться на работу мечты" completed={false} /> {/* ... а что если нужно добавить ещё 10 задач? */} </ul> ); }
Такой подход не масштабируется. Мы не можем заранее знать, сколько задач будет у пользователя. Нам нужен способ взять массив данных (например, из state или из пропсов) и «на лету» преобразовать его в коллекцию React-элементов. Именно для этого мы и используем JavaScript прямо в JSX с помощью фигурных скобок {}.
И здесь на сцену выходит мощный и элегантный метод массивов JavaScript, map().
Метод map()
Метод map() это один из самых частоиспользуемых методов в функциональном программировании на JavaScript и в React он стал практически синонимом рендеринга списков. Давайте вспомним, что он делает.
map() принимает функцию-колбэк и применяет её к каждому элементу исходного массива. Результатом его работы является новый массив, состоящий из результатов вызова этой функции-колбэка для каждого элемента. Исходный массив при этом не изменяется.
Проще говоря, map() преобразует один массив в другой.
Пример с числами:
const numbers = [1, 2, 3, 4, 5]; const doubledNumbers = numbers.map(number => number * 2); console.log(doubledNumbers); // [2, 4, 6, 8, 10]
Мы преобразовали массив чисел в массив этих же чисел, умноженных на два.
Теперь давайте применим эту магию к React. Мы можем преобразовать массив данных в массив React-элементов.
Базовый пример: Рендеринг списка имён
Представьте, что у нас есть массив имён:
const names = ['Анна', 'Иван', 'Мария', 'Петр'];
Наша цель отрендерить неупорядоченный список (<ul>), где каждое имя будет находиться в элементе списка (<li>).
function NamesList() { const names = ['Анна', 'Иван', 'Мария', 'Петр']; return ( <ul> {names.map(name => { return <li>{name}</li>; })} </ul> ); }
Что здесь произошло?
-
Мы объявили массив
names. -
Внутри JSX мы открыли фигурные скобки
{}, чтобы выполнить JavaScript-выражение. -
Мы вызвали
names.map(). Наша функция-колбэк выполнится для каждого элемента массива. -
Для строки
'Анна'функция вернёт<li>Анна</li>. -
Для строки
'Иван'—<li>Иван</li>и так далее. -
Метод
map()соберёт все эти возвращённые значения в новый массив:[<li>Анна</li>, <li>Иван</li>, <li>Мария</li>, <li>Петр</li>]. -
React увидит этот массив элементов внутри тега
<ul>, корректно отрендерит его и превратит в DOM-узлы.
Мы можем записать это ещё короче, используя неявный возврат (implicit return) в стрелочной функции:
function NamesList() { const names = ['Анна', 'Иван', 'Мария', 'Петр']; return ( <ul> {names.map(name => <li>{name}</li>)} </ul> ); }
Мы получили динамический список. Если бы массив names хранился в состоянии (state) и менялся, наш список автоматически бы перерисовывался. Это и есть мощь React в связке с JavaScript.
Но откройте консоль браузера в этом примере. Вы увидите знакомое для многих разработчиков предупреждение: Warning: Each child in a list should have a unique "key" prop. («Каждый дочерний элемент в списке должен иметь уникальный проп key«).
И это подводит нас ко второй, не менее важной части нашего урока.
Важность атрибута key
Итак, React ругается на отсутствие key. Давайте разберёмся, что это такое и зачем он нужен.
Key (ключ) это специальный строковый атрибут, который необходимо включать при создании списков элементов. Ключи помогают React идентифицировать, какие элементы были изменены, добавлены или удалены. Это критически важно для правильной и эффективной работы React с динамическими списками.
Почему ключи так важны? Проблема без ключей.
Чтобы понять необходимость ключей, давайте заглянем «под капот» React. Допустим, у нас есть список из трёх задач и мы рендерим его без ключей.
// Исходный список const todos = [{text: 'Сделать зарядку'}, {text: 'Написать код'}, {text: 'Прочитать книгу'}]; // Рендеринг <ul> {todos.map(todo => <li>{todo.text}</li>)} </ul>
React увидит примерно такую структуру:
<ul> <li>Сделать зарядку</li> <li>Написать код</li> <li>Прочитать книгу</li> </ul>
Теперь представьте, что пользователь удалил первую задачу «Сделать зарядку». Массив в состоянии обновился и теперь выглядит так: [{text: 'Написать код'}, {text: 'Прочитать книгу'}].
Что делает React? Он получает новый массив элементов для рендеринга. Он сравнивает старый виртуальный DOM с новым. Без ключей он не может быть уверен, какой именно элемент исчез. Он видит, что первый <li> изменился с «Сделать зарядку» на «Написать код», второй с «Написать код» на «Прочитать книгу», а третий пропал. В лучшем случае это приведёт к неоптимальному обновлению, а в худшем к багам, особенно если элементы списка имеют собственное состояние (state) или являются компонентами с побочными эффектами.
Как ключи решают проблему?
Когда мы добавляем уникальные ключи, React получает «идентификаторы» для каждого элемента.
const todos = [ {id: 1, text: 'Сделать зарядку'}, {id: 2, text: 'Написать код'}, {id: 3, text: 'Прочитать книгу'} ]; // Рендеринг с ключами <ul> {todos.map(todo => <li key={todo.id}>{todo.text}</li>)} </ul>
Теперь при удалении первой задачи React видит, что исчез элемент с key="1". Элементы с key="2" и key="3" остались на своих местах. React может точечно и максимально эффективно удалить из DOM только тот узел, который соответствует key="1", не трогая и не пересоздавая остальные. Это даёт огромный прирост производительности для больших списков и гарантирует корректность обновлений.
Правила использования ключей:
-
Ключи должны быть уникальными среди соседних элементов. Ключи должны быть уникальными только в рамках одного списка. В разных списках можно использовать одинаковые ключи.
-
Ключи не должны меняться. Не используйте
Math.random()для генерации ключей! При каждом рендере будет создаваться новый ключ, что заставит React каждый раз пересоздавать DOM-узел с нуля, что очень неэффективно. Ключ должен оставаться неизменным для одного и того же элемента на протяжении всех рендеров.
Что использовать в качестве ключа?
-
Идеальный вариант: ID из данных. Если ваши данные приходят с бэкенда, почти всегда у них есть уникальный
id. Используйте его. -
Если ID нет: В крайнем случае, можно использовать индекс элемента в массиве (
index). Это лучше, чем ничего, но у этого подхода есть серьёзные недостатки.
// НЕИДЕАЛЬНО, но иногда допустимо для статических списков <ul> {todos.map((todo, index) => <li key={index}>{todo.text}</li>)} </ul>
Почему индекс плохой ключ?
Представьте, что мы вставляем новый элемент в начало списка. Индекс каждого существующего элемента изменится. Элемент, который был с key={0} («Сделать зарядку»), станет key={1}. React, увидев это, подумает, что элемент с key={0} исчез, а вместо него появились новые элементы с key={1} и key={2} и может начать пересоздавать элементы, теряя их внутреннее состояние (например, состояние чекбокса «выполнено») и приводя к проблемам с производительностью.
Вывод: Всегда используйте стабильный, уникальный идентификатор из данных. Прибегайте к индексу только тогда, когда список статичен (не меняется порядок, нет добавления/удаления элементов) и у вас точно нет другого выбора.
Практические задачи и примеры кода
Мы создадим несколько мини-проектов, которые покажут всю мощь рендеринга списков.
Задача 1: Умный список продуктов
Создадим компонент для отображения списка покупок. Данные будут храниться в состоянии. Мы не только отрендерим список, но и добавим возможность отмечать продукты как купленные.
import React, { useState } from 'react'; function ShoppingList() { // Начальный state с массивом продуктов const [products, setProducts] = useState([ { id: 1, name: 'Молоко', purchased: false }, { id: 2, name: 'Хлеб', purchased: false }, { id: 3, name: 'Яйца', purchased: true }, ]); // Функция для переключения статуса "куплено" const togglePurchased = (productId) => { setProducts(products.map(product => { // Если id совпал, меняем флаг purchased на противоположный if (product.id === productId) { return { ...product, purchased: !product.purchased }; } return product; })); }; return ( <div> <h2>Мой список покупок</h2> <ul> {products.map(product => ( <li key={product.id} // Используем ID как ключ // Меняем стиль в зависимости от статуса style={{ textDecoration: product.purchased ? 'line-through' : 'none', cursor: 'pointer' }} // При клике вызываем функцию переключения onClick={() => togglePurchased(product.id)} > {product.name} </li> ))} </ul> </div> ); } export default ShoppingList;
Что мы здесь сделали:
-
Использовали хук
useStateдля хранения массива продуктов. У каждого продукта естьid,nameи флагpurchased. -
В рендере использовали
products.map()для преобразования массива объектов в массив элементов<li>. -
Каждому
<li>задали уникальныйkey={product.id}. -
Добавили интерактивности: при клике на продукт вызывается функция
togglePurchased, которая обновляет state, создавая новый массив с изменённым флагомpurchasedдля конкретного продукта. -
Стиль текста (зачёркивание) динамически меняется в зависимости от состояния
product.purchased.
Это классический пример работы с динамическими списками в React. Обратите внимание, как важно иметь стабильный id для корректного обновления.
Задача 2: Список пользователей с компонентом
Часто элементы списка сами по себе являются сложными компонентами. Давайте вынесем элемент списка пользователей в отдельный компонент UserCard.
import React from 'react'; // Дочерний компонент для отображения карточки пользователя function UserCard({ user }) { return ( <div style={{ border: '1px solid #ccc', padding: '10px', margin: '10px', borderRadius: '5px' }}> <h3>{user.name}</h3> <p>Email: {user.email}</p> <p>Город: {user.address.city}</p> {/* Предположим, что есть вложенность */} </div> ); } // Родительский компонент, который рендерит список function UsersList() { const [users, setUsers] = useState([ { id: 101, name: 'Алиса', email: 'alice@example.com', address: { city: 'Москва' } }, { id: 102, name: 'Боб', email: 'bob@example.com', address: { city: 'Санкт-Петербург' } }, { id: 103, name: 'Charlie', email: 'charlie@example.com', address: { city: 'Казань' } }, ]); return ( <div> <h1>Наша команда</h1> {/* Рендерим список, передавая каждому компоненту данные и КЛЮЧ */} {users.map(user => ( <UserCard key={user.id} user={user} /> ))} </div> ); } export default UsersList;
Ключевой момент здесь: Ключ key нужно указывать непосредственно на компоненте, который является непосредственным потомком в массиве (в нашем случае на <UserCard />), а не на элементе <div> внутри самого компонента UserCard. Это позволяет React однозначно идентифицировать компонент при изменениях в списке.
Задача 3: Динамическое добавление и удаление элементов
Давайте создадим небольшой проект, где можно добавлять и удалять элементы из списка. Это окончательно закрепит понимание того, как работают ключи и обновления.
import React, { useState } from 'react'; function DynamicNotes() { const [notes, setNotes] = useState([]); const [inputValue, setInputValue] = useState(''); // Добавление новой заметки const addNote = () => { if (inputValue.trim()) { // Проверяем, что поле не пустое const newNote = { // В реальном приложении id должен генерироваться сервером или библиотекой id: Date.now(), // Используем временную метку как уникальный ID (для примера!) text: inputValue }; setNotes([...notes, newNote]); // Добавляем новую заметку в конец массива setInputValue(''); // Очищаем input } }; // Удаление заметки по ID const deleteNote = (id) => { setNotes(notes.filter(note => note.id !== id)); // Оставляем только те заметки, id которых не равен переданному }; return ( <div> <h2>Мои заметки</h2> <div> <input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} placeholder="Введите новую заметку..." /> <button onClick={addNote}>Добавить</button> </div> <ul> {notes.map(note => ( <li key={note.id}> {note.text} {/* Кнопка для удаления. Передаём id заметки в функцию */} <button onClick={() => deleteNote(note.id)} style={{ marginLeft: '10px' }}> Удалить </button> </li> ))} </ul> </div> ); } export default DynamicNotes;
Разбор:
-
Добавление. Мы создаём новый массив, разворачивая в него старые заметки
...notesи добавляя новуюnewNote. Обратите внимание,idмы генерируем какDate.now(). В продакшене для этого лучше использовать специализированные библиотеки (например,uuid), но для учебного примера сойдёт. -
Удаление. Мы используем метод массива
filter(), который возвращает новый массив, исключив из него заметку с темid, которую мы хотим удалить. -
Ключи. Каждая заметка в списке имеет стабильный
key={note.id}, который не меняется на протяжении её «жизни» в списке. Когда мы удаляем одну заметку, React по ключу понимает, какую именно нужно удалить из DOM и делает это максимально эффективно.
Итоги урока
Вы только что освоили одну из самых важных тем в React. Давайте резюмируем:
-
Динамические списки рендерятся с помощью метода массивов
map(), который преобразует массив данных в массив React-элементов. -
Ключ (
key) это специальный и обязательный атрибут, который нужно задавать каждому элементу в массиве. -
Назначение ключа помочь React идентифицировать элементы при изменениях. Это залог корректной работы и высокой производительности ваших списков.
-
Что использовать как ключ? Лучший вариант, это уникальный и неизменный
idиз ваших данных. Индекс массива (index) это запасной, неидеальный вариант, который может привести к проблемам.
Теперь вы можете создавать по-настоящему интерактивные и динамические интерфейсы. Списки это кровь современных веб-приложений и вы только что научились ими управлять.
Чтобы продолжить изучение React и погрузиться в такие темы, как управление состоянием, работа с формами, хуки и создание полноценных приложений, переходите к полному курсу:
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


