Ты неправильно используешь key
- React
- key
- Состояние
- Архитектура
Есть вещи в React, которые все используют, но мало кто понимает до конца. key — одна из них.
Большинство знакомятся с ним так: пишешь .map(), в консоли появляется предупреждение “Each child in a list should have a unique key prop”, добавляешь key={index} — варнинг пропадает, жизнь продолжается.
Но key — это не про списки. Это про идентичность компонента. И как только это понимаешь, открывается целый арсенал трюков.
Как React решает, обновить компонент или пересоздать
Прежде чем говорить о трюках, нужно понять механику. При каждом рендере React сравнивает новое дерево со старым. Встречая компонент, он задаёт себе вопрос: это тот же компонент, что был раньше, или новый?
Ответ определяется двумя вещами: тип компонента и key. Если оба совпадают — React обновляет существующий экземпляр, сохраняя его состояние. Если что-то изменилось — размонтирует старый и монтирует новый с нуля.
// React видит тот же <UserCard> на том же месте → обновляет пропсы, стейт сохраняется
<UserCard userId={1} />
<UserCard userId={2} /> // просто изменились пропсы
// React видит другой key → размонтирует первый, монтирует второй
<UserCard key="user-1" userId={1} />
<UserCard key="user-2" userId={2} /> // новый экземпляр, стейт сброшен
Именно это свойство делает key таким мощным инструментом.

Трюк №1: сброс состояния через key
Представь классическую ситуацию: форма редактирования профиля. Пользователь открыл профиль Алисы, начал что-то менять, потом переключился на профиль Боба — и увидел несохранённые данные Алисы в полях.
function ProfileEditor({ userId }) {
const [name, setName] = useState('');
const [bio, setBio] = useState('');
useEffect(() => {
fetchUser(userId).then(user => {
setName(user.name);
setBio(user.bio);
});
}, [userId]);
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
<textarea value={bio} onChange={e => setBio(e.target.value)} />
</form>
);
}
Стандартное решение — сбрасывать стейт в useEffect при смене userId. Но это приводит к лишнему рендеру: сначала компонент рендерится со старыми данными, потом эффект запускается и обнуляет их.
Элегантное решение — просто добавить key:
// В родительском компоненте
<ProfileEditor key={userId} userId={userId} />
Теперь при смене userId React видит новый key и полностью пересоздаёт компонент. Никакого лишнего рендера, никакого useEffect для сброса, никаких старых данных. Компонент стартует с чистого листа.

Это особенно удобно для:
- Форм с несколькими режимами (создание / редактирование)
- Компонентов с внутренними таймерами или анимациями
- Любого компонента, который должен “не знать” о предыдущей жизни
Трюк №2: key вне списков
Вот что большинство не знает: key можно вешать на любой компонент, не только внутри .map().
// Это абсолютно легальный React
function App() {
const [mode, setMode] = useState('dark');
return (
<ThemeProvider key={mode} theme={mode}>
<Dashboard />
</ThemeProvider>
);
}
При смене mode весь поддерево под ThemeProvider будет пересоздано заново. Иногда это именно то, что нужно — особенно если внутри есть компоненты с состоянием, которое должно сброситься вместе с темой.
Другой пример: страницы в роутере. Если два роута рендерят один и тот же компонент, React по умолчанию не пересоздаёт его — просто меняет пропсы. Это может быть неожиданным:
// Переход между /users/1 и /users/2 НЕ пересоздаёт UserPage
<Route path="/users/:id" element={<UserPage />} />
// С key — пересоздаёт при каждой смене id
<Route
path="/users/:id"
element={<UserPage key={params.id} />}
/>

Про уникальность: глобальная vs локальная
Распространённое заблуждение — key должен быть уникальным во всём приложении. Это не так.
Key должен быть уникальным только среди соседних элементов. React смотрит на keys только в пределах одного родителя.
function App() {
return (
<>
<ul>
<li key="1">Первый список, элемент 1</li>
<li key="2">Первый список, элемент 2</li>
</ul>
<ul>
<li key="1">Второй список, элемент 1</li> {/* key="1" снова — и это нормально */}
<li key="2">Второй список, элемент 2</li>
</ul>
</>
);
}
React не перепутает эти элементы, потому что они находятся в разных родителях. Scope у key — локальный.
Это важно знать при генерации keys для вложенных структур. Нет смысла делать глобально уникальные идентификаторы — достаточно уникальности в пределах одного списка.
// ✅ Достаточно — id уникален в пределах категории
categories.map(category => (
<div key={category.id}>
{category.items.map(item => (
<Item key={item.id} item={item} /> // item.id уникален внутри category.items
))}
</div>
))
Key и анимации: подводный камень
Раз уж key вызывает размонтирование, он напрямую влияет на анимации. Это место, где многие обжигаются.
Допустим, у тебя есть компонент с анимацией появления и исчезновения через Framer Motion:
function Notification({ message, id }) {
return (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }} // exit-анимация
>
{message}
</motion.div>
);
}
Если ты меняешь key у этого компонента, React немедленно размонтирует его — exit-анимация не успевает сыграть. Компонент просто исчезает.
// ❌ exit-анимация не сыграет — компонент сразу исчезнет
<Notification key={notificationId} message={text} />
Решение — обернуть в AnimatePresence. Он перехватывает размонтирование и даёт компоненту доиграть exit-анимацию перед удалением из DOM:
// ✅ exit-анимация отработает корректно
<AnimatePresence mode="wait">
<Notification key={notificationId} message={text} />
</AnimatePresence>

То же самое актуально для React Transition Group и любых других библиотек анимаций, которые работают с монтированием/размонтированием. Всегда проверяй, что смена key не убивает exit-анимацию.
Итог
key — это не атрибут для подавления варнингов. Это способ сказать React: «это другой компонент, не тот что был раньше».
Понимание этого открывает несколько мощных паттернов: сброс состояния без useEffect, контроль над жизненным циклом компонентов вне списков, предсказуемое поведение анимаций. И главное — делает код чище: вместо сложной логики сброса стейта иногда достаточно одного атрибута.