React обеспечивает предсказуемость и прозрачность UI при условии соблюдения его модельных ограничений: однонаправленный поток данных, детерминированный рендер на основе props и state, а также явное управление побочными эффектами. На практике даже опытные команды допускают архитектурные и эксплуатационные огрехи: сохраняют производные значения в состоянии, инициируют внешний код в эффектах без необходимости, пересоздают функции на каждом рендере, маскируют ошибки ремоунтом по key и преждевременно применяют мемоизацию. Последствия — избыточные рендеры, мерцания интерфейса, нестабильные эффекты, расхождения данных и дефекты, плохо поддающиеся диагностике.
В этом материале систематизированы типовые антипаттерны работы с хуками, состоянием, мемоизацией и структурой компонентов. Для каждого случая приведены минимальные примеры и рациональные альтернативы с обоснованием, основанным на принципах: единый источник истины, стабильность зависимостей, отсутствие условных вызовов хуков, четкие границы ответственности и минимизация побочных эффектов. Применение этих практик снижает частоту повторных рендеров, повышает предсказуемость поведения и улучшает сопровождаемость кода как в разработке, так и в продакшене.
Содержание
Не используйте переменные вместо useState для хранения состояния
Используйте useCallback для предотвращения пересоздания функций
Использование useCallback для предотвращения лишних срабатываний useEffect
Добавляйте пустой массив зависимостей к useEffect, если зависимостей нет
Всегда указывайте все зависимости в useEffect и других React‑хуках
Не оборачивайте внешние функции в useCallback без необходимости
Не используйте хуки внутри условных операторов (никаких условных хуков)
Не используйте хуки после return (или внутри условных операторов)
Не заставляйте родительский компонент решать, должен ли дочерний рендериться
Используйте useRef вместо useState, если не нужен повторный рендеринг компонента
Использование props или context в качестве начального состояния
1. useState и useEffect для синхронных данных
Проблема
Частая ошибка: использовать хуки useState
и useEffect
для вычисления производных, синхронных значений, когда все необходимые данные уже доступны при рендере.
Это приводит к ненужной сложности, ухудшению производительности и появлению трудноуловимых багов.
Плохой пример
import React, { useState, useEffect } from 'react';
interface Props {
firstName: string;
lastName: string;
}
function MyComponent(props: Props) {
const { firstName, lastName } = props;
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return (
<span>{fullName}</span>
);
}
Возникающие проблемы
useEffect
срабатывает после рендера, значит сначала пользователь увидит устаревшее значение;Для обновления значения требуется как минимум два рендера;
Код менее прозрачен и сложнее для поддержки.
Почему это плохо
Задержка обновления значения: отображается устаревшее значение, пока не сработает
useEffect
;Лишний render:
useEffect
вызываетsetState
→ компонент рендерится снова;Лишняя сложность: усложняет логику компонента — вычисление, которое можно сделать сразу, становится запутанным;
Неэффективность: такой подход не даёт преимущества, а только добавляет ненужные эффекты и риск багов.
Как стоит делать
import React from 'react';
interface Props {
firstName: string;
lastName: string;
}
function MyComponent(props: Props) {
const { firstName, lastName } = props;
const fullName = `${firstName} ${lastName}`;
return (
<span>{fullName}</span>
);
}
Почему так лучше
Простота: нет состояния или эффекта — компонент проще и легче читать;
Нет устаревших данных: новое значение мгновенно отражается при изменении пропсов;
Один render: компонент обновляется ровно один раз, когда это действительно нужно;
Оптимальный UX: пользователь всегда видит актуальную информацию без визуальных артефактов или миганий.
2. Не используйте переменные вместо useState для хранения состояния
Проблема
Частая ошибка: создавать значения состояния обычной переменной внутри компонента вместо использования хуков вроде useState
или useReducer
.
Это приводит к непредсказуемому поведению — каждое новое обновление вызывает повторное создание переменной, из-за чего ломается мемоизация, эффекты и оптимизации React.
Плохой пример
import AnotherComponent from 'components/AnotherComponent';
function Component() {
// Не делайте так!
const value = { someKey: 'someValue' };
return <AnotherComponent value={value} />;
}
Возникающие проблемы
На каждом рендере создаётся новый объект
value
со свежей ссылкой;Любые мемоизации (
React.memo
,useMemo
,useCallback
) становятся бесполезными — React видит новое значение каждый раз;Вложенные эффекты (
useEffect
), зависящие от такого значения, будут срабатывать на каждом рендере;Непредсказуемые ре-рендеры дочерних компонентов.
Почему это плохо
Срывается оптимизация: React не может определить, изменилось ли значение;
Лишние render’ы: даже если данные не изменились, дочерние компоненты будут перерисовываться;
Бесполезна мемоизация: невозможно эффективно использовать
memo
,useMemo
и прочее;Обилие багов: неправильный порядок эффектов, "дрожащие" интерфейсы.
Как стоит делать
import { useState } from 'react';
import AnotherComponent from 'components/AnotherComponent';
function Component() {
// Делайте так!
const [value, setValue] = useState({ someKey: 'someValue' });
return <AnotherComponent value={value} />;
}
Почему так лучше
Стабильная ссылка: пока не вызван
setValue
, ссылка наvalue
не меняется;Работает мемоизация: React может правильно оптимизировать перерисовки;
Правильная работа эффектов:
useEffect
с этой зависимостью сработает только по изменению значения.
Особый случай: действительно неизменяемое значение
Если значение не будет никогда меняться (например, константа), объявляйте его снаружи компонента — тогда ссылка всегда будет одной и той же между всеми рендерами.
// Можно делать так, если value не будет обновляться
const value = { someKey: 'someValue' };
function Component() {
return <AnotherComponent value={value} />;
}
3. Описывайте CSS вне компонента при использовании CSS-in-JS
Проблема
Частая ошибка: определять стили внутри тела компонента при использовании CSS-in-JS библиотек.
Это приводит к тому, что объект стиля или класс создаётся заново на каждом рендере, что увеличивает нагрузку на JS, разрушает оптимизацию, и может вызывать лишние пересоздания или эффекты.
Плохой пример
import makeCss from 'some/css/in/js/library';
function Component() {
// Не делайте так!
return <div className={makeCss({ background: 'red', width: '100%' })} />;
}
Возникающие проблемы
Новый объект: каждый рендер создаёт новый объект со свежей ссылкой;
Бесполезна мемоизация: CSS-класс будет пересоздаваться, даже если стили не изменились;
Потери производительности: на больших компонентах это приводит к ненужным перерасчётам и замедлениям;
Повышается риск багов: могут неправильно сработать зависимости (
useEffect
,React.memo
и др.).
Почему это плохо
Избыточная генерация классов: библиотека заново создаёт CSS-класс на каждый рендер, что забивает память и мешает кешированию;
Лишние вычисления в JS: ресоздание объектов занимает CPU время;
Плохая интеграция с оптимизациями React: мемоизация невозможна, эффекты триггерятся лишний раз.
Как стоит делать
import makeCss from 'some/css/in/js/library';
// Описывайте стили ОДИН РАЗ вне компонента!
const someCssClass = makeCss({
background: 'red',
width: '100%'
});
function Component() {
return <div className={someCssClass} />;
}
Почему так лучше
Один объект: CSS-класс создаётся один раз — при загрузке модуля, а не при каждом рендере;
Лучшее кеширование: стили создаются только при необходимости, экономится память;
Более предсказуемое поведение: не происходит неожиданных пересозданий/эффектов;
Максимальная производительность: минимизируется работа по генерации/применению стилей.
4. Используйте useCallback для предотвращения пересоздания функций
Проблема
Частая ошибка: объявлять функции-обработчики прямо внутри функциональных компонентов без мемоизации, из-за чего при каждом рендере создаётся новый экземпляр функции. Это нарушает оптимизацию, приводит к нежелательным перерисовкам мемоизированных компонентов и потере эффективности хуков, зависящих от функций.
Плохой пример
import { useState } from 'react';
function Component() {
const [value, setValue] = useState(false);
// Эта функция пересоздаётся на каждом рендере!
const handleClick = () => {
setValue(true);
};
return <button onClick={handleClick}>Click me</button>;
}
Возникающие проблемы
Несовместимость с мемоизацией: дочерние компоненты с
React.memo
или функции-хуки (useMemo
,useEffect
, принимающие эту функцию в deps) будут срабатывать при каждом рендере, потому что передаётся новая ссылка на функцию;Лишние обновления: компоненты или мемо-объекты обновляются, даже если логика не поменялась;
Падение производительности: становится невозможным предсказуемо управлять ререндерами.
Почему это плохо
Бесполезна мемоизация: если референс функции всегда новый,
React.memo
,useMemo
иuseEffect
становятся бессмысленными для оптимизации;Лишняя нагрузка на приложение: увеличивается число рендеров, что негативно влияет на производительность, особенно в больших проектах;
Могут появиться баги: если подписались на какой-то эффект с функцией в зависимостях — этот эффект будет запускаться снова и снова;
Путается дерево компонентов: дочери не могут "полагаться" на стабильность пропсов-функций.
Как стоит делать
import { useState, useCallback } from 'react';
function Component() {
const [value, setValue] = useState(false);
// Сохраняем функцию stable, пока не изменятся зависимости
const handleClick = useCallback(() => {
setValue(true);
}, []); // Зависимости — пустой массив: функция не будет пересоздаваться после первого рендера
return <button onClick={handleClick}>Click me</button>;
}
Почему так лучше
Стабильная ссылка на функцию:
handleClick
сохраняет одну и ту же ссылку между рендерами, если не меняются зависимости;Корректная работа мемоизации: работает оптимизация
React.memo
,useMemo
иuseEffect
по зависимостям — лишних обновлений нет;Прогнозируемая производительность: функции не пересоздаются без необходимости, что особенно важно в сложных/больших компонентах;
Меньше багов: снижается риск неожиданных перерендеров или зацикливания эффектов из-за “новых” функций.
Когда useCallback стоит и не стоит использовать
Когда useCallback действительно нужен
Когда функция передаётся в мемоизированный компонент (
React.memo
,useMemo
,useEffect
), чтобы их оптимизация работала корректно;Когда требуется сохранить стабильно одну и ту же ссылку на функцию между рендерами, чтобы не рушить мемоизацию или не вызывать лишние сайд-эффекты.
Когда useCallback не нужен
Если функция никогда не передаётся наружу (“вниз” по дереву), и не участвует в зависимостях эффектов/мемоизации — мемоизация избыточна;
Для очень коротких компонентов, где лишние пересоздания не влияют на производительность — избыточное использование
useCallback
может даже усложнить восприятие кода.
5. useCallback для стабильных зависимостей
Проблема
Частая ошибка: передавать функции напрямую в зависимости для useMemo
, мемуизированных компонентов (React.memo
) или других хуков — без использования useCallback
.
Это приводит к тому, что даже не изменившиеся логические части приложения становятся причиной лишних пересозданий значений и перерисовок, потому что ссылка на функцию меняется при каждом рендере.
Плохой пример
import React, { memo, useMemo } from 'react';
const MemoizedChildComponent = memo(({ onTriggerFn }) => {
// Какой-то код...
});
function Component({ someProp }) {
// Функция пересоздаётся при каждом рендере!
const onTrigger = () => {
// Какая-то логика...
};
// memoizedValue будет пересчитываться каждый рендер,
// потому что onTrigger — новая функция на каждой итерации!
const memoizedValue = useMemo(() => {
// Какая-то дорогая операция...
}, [onTrigger]);
// MemoizedChildComponent будет ОТКАТЫВАТЬ мемоизацию — и перерендериваться —
// потому что onTriggerFn меняется по ссылке при каждом рендере родителя.
return (
<>
<MemoizedChildComponent onTriggerFn={onTrigger} />
<button onClick={onTrigger}>Click me</button>
</>
);
}
Возникающие проблемы
Лишние пересчёты: значения
useMemo
пересчитываются без необходимости;Бесполезные rerender-ы: даже мемуизированные дочерние компоненты ре-рендерятся из-за смены ссылки на функцию;
Потери производительности: увеличение количества вычислений и ререндеров.
Почему это плохо
Функция или значение становится "нестабильной" зависимостью;
Поведенческие баги: эффекты и вычисления срабатывают чаще ожидаемого;
Нет смысла в
React.memo
иuseMemo
, если их зависимости не стабильны.
Как стоит делать
import React, { memo, useCallback, useMemo } from 'react';
const MemoizedChildComponent = memo(({ onTriggerFn }) => {
// Какой-то код...
});
function Component({ someProp }) {
// Функция мемоизирована по ссылке — будет менять значение только при смене someProp
const onTrigger = useCallback(() => {
// Какая-то логика...
}, [someProp]);
// Теперь useMemo зависит от onTrigger — но функция будет сохраняться между рендерами,
// пока не изменился someProp.
const memoizedValue = useMemo(() => {
// Какая-то дорогая операция...
}, [onTrigger]);
// Дочерний компонент не будет получать новый проп onTriggerFn без необходимости,
// не будет лишних ререндеров.
return (
<>
<MemoizedChildComponent onTriggerFn={onTrigger} />
<button onClick={onTrigger}>Click me</button>
</>
);
}
Почему так лучше
Стабильные зависимости:
onTrigger
сохраняет ссылку между рендерами, если deps не изменились;Меньше лишних вычислений:
useMemo
работает эффективно;Прирост производительности: дочерние мемуизированные компоненты не рендерятся "зря";
Прозрачная логика: проще поддерживать и дебажить, меньше "магии".
6. Использование useCallback для предотвращения лишних срабатываний useEffect
Проблема
Частая ошибка: передавать не мемоизированные функции (например, обработчики событий) в массив зависимостей useEffect
. Это приводит к повторному выполнению эффекта на каждом рендере, даже если по сути зависимости не изменились. В результате:
Эффекты выполняются чаще, чем нужно;
Возникают лишние перерендеры и потенциальные баги;
Страдает производительность и предсказуемость кода.
Плохой пример
import { useEffect } from 'react';
const Component = ({ someProp }) => {
const onTrigger = () => {
// ...logic
};
useEffect(() => {
// ...effect logic
}, [onTrigger]); // <- onTrigger создаётся заново на каждом рендере!
return <button onClick={onTrigger}>Click me</button>;
}
Возникающие проблемы
useEffect
будет срабатывать на каждый рендер, потому чтоonTrigger
— новая функция на каждом шаге;Теряется контроль за воспроизведением эффекта;
Код становится менее предсказуемым и труднее для отладки.
Почему это плохо
Снижается производительность: эффекты запускаются чаще, чем это реально нужно;
Осложнение логики: приходится искусственно оптимизировать зависимости;
Потенциальные баги: side-эффекты могут некорректно срабатывать или приводить к зацикливанию.
Как стоит делать
Используйте useCallback
для мемоизации функций — так их ссылка будет меняться только когда реально поменялись присутствующие переменные. Благодаря этому, useEffect
будет запускаться только тогда, когда в этом есть реальная необходимость.
import { useCallback, useEffect } from 'react';
const Component = ({ someProp }) => {
const onTrigger = useCallback(() => {
// ...logic
}, [someProp]); // функция будет обновляться только при изменении someProp
useEffect(() => {
// ...effect logic
}, [onTrigger]); // теперь useEffect зависит от стабильной функции
return <button onClick={onTrigger}>Click me</button>;
}
Почему так лучше
useEffect
запускается только при реальных изменениях зависимостей;Меньше лишних рендеров и вызовов эффекта — компонент работает быстрее и стабильнее;
Код легко читать: dependencies
useEffect
иuseCallback
выражают реальную логику зависимости;Меньше неожиданных багов, связанных с повторным созданием функций.
7. Добавляйте пустой массив зависимостей к useEffect, если зависимостей нет
Проблема
Частая ошибка — не указывать массив зависимостей в useEffect
, когда он не зависит ни от каких переменных. Это приводит к тому, что эффект выполняется после каждого рендера компонента, а не только один раз при монтировании, что неэффективно и часто приводит к ошибкам.
Плохой пример
import { useEffect } from 'react';
const Component = () => {
useEffect(() => {
// Некоторая логика
// Не делайте так!
})
return <div>Example</div>;
}
Возникающие проблемы
useEffect
будет выполняться на каждом рендере, а не только при монтировании;Вызывается лишний код, что может приводить к падению производительности;
В особо сложных случаях могут появиться баги или неверная обработка данных (например, если внутри эффекта есть запрос к серверу или подписка).
Почему это плохо
Неэффективность: любой код в
useEffect
― даже тяжелый или асинхронный ― будет повторяться при каждом рендере;Трудности поддержки: бывает сложно понять, почему
useEffect
вызывается так часто;Повышенный риск багов, особенно с побочными эффектами (например, утечками памяти, повторным созданием подписок и т. п.).
Как стоит делать
Всегда указывайте пустой массив зависимостей ([]
), если эффект должен выполниться только один раз при монтировании компонента.
import { useEffect } from 'react';
const Component = () => {
useEffect(() => {
// Некоторая логика
// Так правильно!
}, [])
return <div>Example</div>;
}
Почему так лучше
Эффект выполняется ровно один раз — при первом рендере (монтировании);
Код работает быстрее, особенно если внутри эффекта есть тяжелые вычисления или сетевые запросы;
Нет риска неоднократных подписок или дублирования побочных действий;
Ваши эффекты ведут себя предсказуемо, как аналог
componentDidMount
в классических компонентахReact
.
Важно
Аналогичная логика применима к другим React
-хукам — useCallback
и useMemo
: если результат не зависит ни от каких внешних переменных, либо не используйте эти хуки вовсе, либо обязательно передавайте им пустой массив зависимостей, чтобы мемоизация происходила правильно.
8. Всегда указывайте все зависимости в useEffect и других React-хуках
Проблема
Распространённая ошибка — не добавлять все используемые переменные и функции в массив зависимостей хуков (useEffect
, useCallback
, useMemo
). Игнорирование этой практики приводит к тому, что эффект или функция работают с устаревшими значениями, иногда вызывая трудновоспроизводимые баги, проблемы с производительностью и ошибочное поведение.
Плохой пример
import { useEffect, useState } from 'react';
const Component = () => {
const [value, setValue] = useState();
useEffect(() => {
// Здесь используется value, но он не указан в зависимостях!
// Это приведёт к тому, что эффект всегда будет видеть исходное значение value.
}, []);
return <div>{value}</div>;
}
Возникающие проблемы
Эффект использует устаревшие значения переменных, так как запускается только один раз и не реагирует на обновления;
Некорректное отображение данных, неправильная логика работы, невозможность отследить источник бага;
Будущие версии
React
(особенно в строгом режимеReact 18+
) или улучшения линтеров могут проявить эти баги в ещё более неожиданных местах.
Почему это плохо
Риск невидимых багов, которые сложно воспроизвести или диагностировать;
Код становится неуправляемым при изменении зависимостей;
При обновлениях
React
поведение кода может изменитьcя непредсказуемо из-за внутренних оптимизаций;ESLint
правилоreact-hooks/exhaustive-deps
специально предупреждает о таких ошибках — не игнорируйте его.
Как стоит делать
Всегда добавляйте в массив зависимостей все переменные и функции, используемые внутри useEffect
(или других хуков).
import { useEffect, useState } from 'react';
const Component = () => {
const [value, setValue] = useState();
useEffect(() => {
if (!value) {
// Эффект запустится снова, если value изменится.
}
}, [value]); // value обязательно в зависимостях!
return <div>{value}</div>;
}
Если вам кажется, что эффект "слишком часто" перезапускается, подумайте:
Можно ли переписать логику эффекта так, чтобы он был идемпотентным?
Можете ли вы контролировать, выполнять ли действия, с помощью
if
внутри эффекта (например, только при определённом значении)?Не используйте эффект там, где он не нужен — возможно, логика должна быть вне
useEffect
.
Почему так лучше
Ваши эффекты всегда получают актуальные данные;
Минимизируется количество трудноуловимых багов;
Код становится предсказуемым и легче поддерживается, особенно при обновлениях React;
Вы следуете
best practices
официальной документации и рекомендациям сообщества.
Особенности функций и мемоизация
Если вы передаёте функцию в зависимости, используйте useCallback или определяйте функцию внутри
useEffect
, чтобы ссылка на функцию не менялась на каждый рендер.Следите за тем, чтобы не включать новые объекты или массивы в зависимости, иначе эффект будет срабатывать при каждом рендере из-за новых ссылок.
9. Не используйте useEffect для инициализации внешнего кода
Проблема
Часто инициализационный код (например, запуск внешней библиотеки) ошибочно помещают в useEffect
с пустым массивом зависимостей. На первый взгляд это может показаться правильным (“сработает один раз при монтировании”), но на самом деле такой подход лишён смысла и подвержен ошибкам. Если инициализация не зависит от состояния компонента или пропсов, то нет причин оборачивать её в useEffect
.
Плохой пример
import { useEffect } from 'react';
import initLibrary from '/libraries/initLibrary';
const Component = () => {
// Не делайте так!
useEffect(() => {
initLibrary()
}, []);
return <div>Example</div>;
}
Возникающие проблемы
Зависимость от жизненного цикла компонента без необходимости;
Может вызвать дублирующуюся инициализацию, если компонент монтируется несколько раз;
Ошибка становится более вероятной при изменении структуры приложения или логике рендеринга;
Бессмысленное усложнение кода.
Почему это плохо
Внешние библиотеки или однократная инициализация должны жить вне
React
-компонента — не “привязывать” их к его жизненному циклу;Сложнее поддерживать: если понадобится переиспользовать логику или переместить код, легко ошибиться;
Нарушает явность: не видно, почему инициализация происходит там, где происходит.
Как стоит делать
Если инициализация не зависит от состояния или пропсов React
-компонента, выполните её вне компонента — сразу при импорте или в модуле.
import initLibrary from '/libraries/initLibrary';
// Сразу при импорте — логика срабатывает один раз при запуске приложения
initLibrary();
const Component = () => {
return <div>Example</div>;
}
Почему так лучше
Код очевиден: инициализация всегда происходит один раз, независимо от
React
;Нет лишних побочных эффектов при каждом монтировании;
Явная структура, легко поддерживать и тестировать;
Приложение работает быстрее, потому что избегает лишних операций.
Важное уточнение
Если действительно необходимо обратиться к состоянию компонента или пропсам для инициализации, только тогда используйте useEffect
— но обязательно следите, чтобы все эти зависимости были перечислены в массиве зависимостей useEffect
.
10. Не оборачивайте внешние функции в useCallback без необходимости
Проблема
Частая ошибка — использовать useCallback
для обёртывания внешних функций, которые просто импортируются из других модулей и не зависят от состояния или props компонента. Это приводит к усложнению кода и бессмысленной нагрузке на React
: он тратит ресурсы на проверку и мемоизацию функции, хотя в этом нет нужды.
Плохой пример
import { useCallback } from 'react';
import externalFunction from '/services/externalFunction';
const Component = () => {
// Не делайте так!
const handleClick = useCallback(() => {
externalFunction()
}, []);
return <button onClick={handleClick}>Click me</button>;
}
Возникающие проблемы
Лишняя “работа”
React
при каждом рендере — проверка, не изменились ли зависимости и нужно ли пересоздавать функцию;Бессмысленное усложнение кода и
API
-компонента;Код труднее читать, а логика использования хуков размывается.
Почему это плохо
Нет причин создавать новую функцию-обёртку для вызова уже стабильной внешней функции;
Меньше кода — проще поддержка;
Меньше
React
-магии — меньше риска ошибиться при рефакторинге;Код становится чище и прозрачнее.
Как стоит делать
Если функция импортируется извне и не зависит от состояния или props компонента — передавайте её напрямую!
import externalFunction from '/services/externalFunction';
const Component = () => {
// Просто используйте функцию напрямую
return <button onClick={externalFunction}>Click me</button>;
}
Почему так лучше
Код проще: Вы избегаете лишней абстракции — сразу видно, какую функцию вы передаёте, не нужно “распутывать” уровни вызовов;
Нет лишних вычислений:
React
не тратит ресурсы на создание, хранение и сравнение мемоизированной функции, когда это абсолютно не требуется;Меньше багов: Нет риска забыть про зависимости или ошибиться с массивом зависимостей, ведь
useCallback
просто не нужен;Оптимальный рендер: Вы не заставляете React проверять и перегенерировать
callback
-и там, где каждый раз можно безопасно использовать одну и ту же внешнюю функцию;Проще поддерживать: Такой код легче читать, понимать и сопровождать — новые разработчики сразу видят, что обработчик события — это внешняя функция, а не некая логика компонента;
Когда useCallback действительно нужен
Используйте useCallback
тогда, когда ваша функция:
Вызывает несколько разных внешних или внутренних функций за один раз;
Зависит от значения состояния (через
useState
,useReducer
) или пропсов, и нужна стабильная ссылка функции (например, для оптимизацииmemo
- илиuseEffect
-зависимостей).
Корректный пример
import { useCallback, useState } from 'react';
import { externalFunction, anotherExternalFunction } from '/services';
const Component = ({ passedInProp }) => {
const [value, setValue] = useState();
// Здесь useCallback оправдан:
const handleClick = useCallback(() => {
externalFunction()
anotherExternalFunction()
setValue(passedInProp) // используем prop и state значение
}, [passedInProp, value]);
return <button onClick={handleClick}>Click me</button>;
}
11. Не используйте useMemo с пустым списком зависимостей
Проблема
Частая ошибка — добавлять useMemo
с пустым списком зависимостей ([])
без веских на то оснований. Это может выглядеть так, будто мы «оптимизируем» вычисления, но на самом деле либо вычисление не зависит ни от чего и его нужно вынести за пределы компонента, либо мы неправильно определили зависимости.
Плохой пример
import { useMemo } from 'react';
const Component = () => {
// Не делайте так.
const memoizedValue = useMemo(() => {
return 3 + 5;
}, []);
return <div>{memoizedValue}</div>;
};
Возникающие проблемы
Это добавляет лишнюю сложность:
useMemo
здесь ничего не «кэширует», потому что результат всегда одинаковый;Ухудшает читаемость кода: почему простое выражение вдруг обернуто хуком?
Вводит других разработчиков в заблуждение: кажется, будто есть «скрытая» зависимость или вычисление тяжёлое, хотя это не так.
Почему это плохо
Нет смысла использовать
useMemo
там, где результат не зависит от пропсов или состояния — результат легко можно посчитать заранее;Лишняя обёртка усложняет компонент и делает логику неочевидной;
Выглядит как преждевременная оптимизация, которая не даёт никакой пользы, но цепляет техдолг.
Как стоит делать
Если выражение не зависит от пропсов или состояния, определяйте его вне компонента, либо сразу в месте использования:
const memoizedValue = 3 + 5;
const Component = () => {
return <div>{memoizedValue}</div>;
};
Почему так лучше
Простота: выражение видно сразу, нет ненужных хуков;
Нет скрытых смыслов: очевидно, что значение не зависит от пропсов и состояния;
Улучшает читаемость: код легче воспринимается и поддерживается;
Избавляет от шума: никакой фиктивной «оптимизации», всё по делу.
12. Не объявляйте компоненты внутри других компонентов
Проблема
Частая ошибка — объявлять дочерний компонент внутри родительского компонента, прямо в его теле.
Плохой пример
const Component = () => {
// Не делайте так.
const ChildComponent = () => {
return <div>I'm a child component</div>;
};
return <div><ChildComponent /></div>;
};
Возникающие проблемы {#voznikayushie-problemy10}
Каждый раз создаётся новая функция, что может приводить к обновлению состояния или эффектов даже там, где этого не ожидалось;
React не распознает, что
ChildComponent
— это тот же самый компонент между рендерами, из-за чего не работает мемоизация (например,React.memo
);Код становится более громоздким и запутанным по мере роста числа вложенных компонентов.
Почему это плохо
Каждый рендер — новое создание функции: Функция
ChildComponent
пересоздаётся каждый раз при любом рендере родителя, что приводит к дополнительным затратам памяти и времени;Нарушение оптимизаций React:
React
не может оптимизировать такие компоненты, ведь с его точки зренияChildComponent
постоянно новая функция;Потеря эффективности хуков: Если
ChildComponent
использует хуки (например,useState
илиuseEffect
), они будут инициализироваться заново при каждом рендере родителя, ломая ожидаемое поведение;Усложнение кода: В сложных файлах появляется множество вложенных объявлений компонентов, код становится менее читаемым и труднее для сопровождения.
Как стоит делать
Перенесите объявление компонента вне тела родительского:
const ChildComponent = () => {
return <div>I'm a child component</div>;
};
const Component = () => {
return <div><ChildComponent /></div>;
};
Или ещё лучше — в отдельный файл для переиспользования и читаемости.
import ChildComponent from 'components/ChildComponent'
const Component = () => {
return <div><ChildComponent /></div>
}
Почему так лучше
Компонент создаётся один раз:
ChildComponent
объявляется один раз для всего приложения, а не для каждого рендера родителя;Оптимизация производительности:
React
может эффективно кэшировать и оптимизировать дочерний компонент;Корректная работа хуков: Вся логика хуков выполняется корректно и ожидаемо;
Чистый и прозрачный код: Файл проще читать и сопровождать, особенно если компонентов становится больше одного.
13. Не используйте хуки внутри условных операторов (никаких условных хуков)
Проблема
Частая ошибка — вызывать хуки (например, useState, useEffect) внутри if, switch, циклов или других условий.
Плохой пример
import { useState } from 'react';
const Component = ({ propValue }) => {
if (!propValue) {
// Не делайте так!
const [value, setValue] = useState(propValue);
}
return <div>{value}</div>;
};
Почему это плохо
Нарушение правил хуков:
React
требует, чтобы хуки вызывались всегда в одном и том же порядке при каждом рендере;Потеря синхронизации: Если хук вызывается только при выполнении условия,
React
не может гарантировать правильное соответствие состояния между разными рендерами. Это приводит к непредсказуемым багам, которые очень сложно отладить;Сбои в работе хуков: При условных вызовах хуки могут внезапно исчезнуть или поменяться местами, что сломает их внутренние данные и приведёт к ошибкам и невозможности правильно обновлять компонент.
Возникающие проблемы
Например,
useState
не сможет обновить значение, так как порядок хуков нарушен.
Как стоит делать
Все хуки должны вызываться без условий — на верхнем уровне тела функции компонента, всегда и в одной и той же последовательности;
Условная логика должна быть только внутри самой функции, а не вокруг вызова хука. Например:
import { useState } from 'react';
const Component = ({ propValue }) => {
const [value, setValue] = useState(propValue);
if (!propValue) {
return <span>No value</span>;
}
return <div>{value}</div>;
};
Как поступать с «условными хуками»
Если хук иногда не нужен, всегда вызывайте его, а затем управляйте поведением через условия внутри самого хука или компонента;
Для переиспользуемой логики — выносите её в кастомные хуки. В параметры кастомного хука добавляйте флаг
enabled
, чтобы внутри хука можно было контролировать, выполнять ли побочные эффекты:
function useCustomHook(enabled) {
useEffect(() => {
if (!enabled) return;
// нужная логика
}, [enabled]);
}
Почему так лучше
Гарантия порядка вызова хуков. React всегда знает, какой хук чему соответствует, и корректно хранит состояние компонентов;
Предсказуемый и стабильный код. Исключаются редкие и очень трудноуловимые ошибки;
Удобство масштабирования и сопровождения. Код всегда выглядит одинаково, проще соблюдать принцип единственности и переиспользовать хуки.
14. Не используйте хуки после return (или внутри условных операторов)
Проблема
Распространённая ошибка — вызывать хуки внутри условных операторов или после оператора return
. Многие разработчики знают правило: хуки нельзя размещать внутри if
, но не всегда замечают, что return тоже создает условие выхода и делает вызов хука непредсказуемым.
Плохой пример
import { useState } from 'react'
const Component = ({ propValue }) => {
if (!propValue) {
return null
}
// Не делайте так: хук будет вызван только если propValue существует.
const [value, setValue] = useState(propValue)
return <div>{value}</div>
}
Возникающие проблемы
Непредсказуемое выполнение хуков: Хуки вызываются только при определённых условиях, из-за чего
React
теряет след за их порядком между разными рендерами;Ошибка исполнения:
React
выдаст ошибку ("Rendered fewer hooks than expected"), поскольку последовательность вызовов хуков нарушается;Нарушение принципов React:
React
ожидает, что хуки всегда вызываются в одном и том же порядке при каждом рендере компонента.
Почему это плохо
Сломанные хуки: Если не все хуки вызваны в одних и тех же условиях,
React
не сможет корректно сопоставлять состояние, эффекты и другие внутренние механизмы хуков между рендерами;Неочевидные баги: Такая ошибка иногда проявляется только при определённых входных данных, поэтому баги могут быть сложноуловимыми и непредсказуемыми;
Падение приложения: При нарушении правила порядок вызова хуков React выбросит ошибку и компонент не сможет корректно отобразиться.
Как стоит делать
Все вызовы хуков должны находиться вне любых условий и всегда вызываться при каждом рендере вашего компонента, вне зависимости от входных данных:
import { useState } from 'react'
const Component = ({ propValue }) => {
// Правильно: хук вызывается всегда, до любых возвращений
const [value, setValue] = useState(propValue)
if (!propValue) {
return null
}
return <div>{value}</div>
}
Или ещё лучше — вынесите логику вне компонента, если это возможно.
Почему так лучше
Правильный порядок выполнения:
React
всегда будет знать, сколько и каких хуков было вызвано, и правильно сопоставит состояния между рендерами;Простое сопровождение: Код становится очевидным и защищённым от ошибок, связанных с порядком вызова хуков;
Соответствие документации: Это прямо соответствует официальным правилам хуков
React
: “Правила хуков: Вызывайте хуки только на верхнем уровне функции компонента”.
15. Не заставляйте родительский компонент решать, должен ли дочерний рендериться
Проблема
Обычная практика — управлять рендерингом дочернего компонента на уровне родителя через условное выражение.
Плохой пример
import { useState } from 'react'
const ChildComponent = ({ shouldRender }) => {
return <div>Rendered: {shouldRender}</div>
}
const Component = () => {
const [shouldRender, setShouldRender] = useState(false)
return <>
{ !!shouldRender && <ChildComponent shouldRender={shouldRender} /> }
</>
}
Возникающие проблемы
Каждый раз, когда условие не выполняется,
React
полностью размонтирует дочерний компонент;Весь внутренний state дочернего компонента сбрасывается (например, форма или значения
useState
);Все
useEffect
/useMemo
/useCallback
будут заново инициализироваться и выполняться при новом рендере;Если компонент выполняет тяжёлые вычисления или сетевые запросы — они также повторяются.
Почему это плохо
Потеря внутреннего состояния: форма или другие данные будут теряться каждый раз при сокрытии компонента;
Избыточные перерендеры: ненужная инициализация хуков и эффектов;
Лишние запросы: можно случайно запускать тяжелые операции при каждом появлении компонента;
Сложность управления множеством условных рендеров в крупных файлах: когда условие приходится писать у каждого компонента по месту вызова.
Как стоит делать
Перенесите условие рендеринга внутрь самого дочернего компонента:
import { useState } from 'react'
const ChildComponent = ({ shouldRender }) => {
if (!shouldRender) {
return null
}
return <div>Rendered: {shouldRender}</div>
}
const Component = () => {
const [shouldRender, setShouldRender] = useState(false)
return <ChildComponent shouldRender={shouldRender} />
}
Почему так лучше
Сохранение состояния: Дочерний компонент остаётся смонтированным, его хуки продолжают работать и state не сбрасывается;
Меньше лишних инициализаций:
useEffect
,useMemo
,useCallback
инициализируются только один раз и не "забывают" свои значения при скрытии;Удобство: Условия управления отображением вынесены в сам компонент, а родитель просто передаёт prop (стиль
single responsibility
);Оптимизация производительности: Не происходит лишних монтирований и размонтирований, не тратятся ресурсы на повторную инициализацию.
Когда так делать не нужно
Если вы хотите, чтобы внутренняя логика дочернего компонента полностью сбрасывалась при выключении (например, для отмены асинхронных запросов, сброса состояния или остановки таймеров), нужно размонтировать компонент на уровне родителя.
Пример, где такой вариант не подходит:
const ChildComponent = ({ shouldRender, someOtherProp }) => {
useEffect(() => {
// Этот код будет выполняться даже при shouldRender === false!
}, [someOtherProp])
if (!shouldRender) {
return null
}
return <div>Rendered: {shouldRender}</div>
}
Пытаться поместить хуки после условия нельзя:
const ChildComponent = ({ shouldRender, someOtherProp }) => {
if (!shouldRender) {
return null
}
// Хук ниже - так писать нельзя, React выдаст ошибку!
useEffect(() => {}, [someOtherProp])
}
Выбор зависит от задачи:
Хотите временно скрыть компонент, сохранив его состояние? Условие внутри дочернего!
Хотите полностью сбросить компонент и его эффекты? Условный рендеринг у родителя!
16. Используйте useReducer вместо множества useState
Проблема
Частая ошибка — раздувать компонент множеством вызовов useState
для хранения связанных частей состояния.
Плохой пример
import { useState } from 'react'
const Component = () => {
// Не делайте так: слишком много useState на связанные значения.
const [text, setText] = useState('')
const [error, setError] = useState('')
const [touched, setTouched] = useState(false)
const handleChange = (event) => {
const value = event.target.value
setText(value)
if (value.length < 6) {
setError('Too short')
} else {
setError('')
}
}
return <>
{!touched && <div>Write something...</div> }
<input type="text" value={text} onChange={handleChange} />
<div>Error: {error}</div>
</>
}
Возникающие проблемы
Избыточные и связанные
useState
делают компонент трудночитаемым и запутанным;Логика обработки состояния размазывается по разным частям кода;
Сложно аккуратно развивать компонент: становится всё труднее добавлять новую логику и управлять обновлениями связанных state.
Почему это плохо
Сложность поддержки: Код с большим количеством
useState
быстро превращается в "лоскутное одеяло", где каждая функция обновляет свой фрагмент состояния;Ошибка синхронизации: Связанные друг с другом
state
-ы могут быть обновлены не вместе и не последовательно;Много ререндеров: Каждый
useState
может вызвать отдельный ререндер, если обновляется по отдельности.
Как стоит делать
При наличии нескольких связанных state
используйте useReducer
— он позволяет централизовать управление состоянием и всей логикой обновления в одном месте:
import { useReducer } from 'react'
const UPDATE_TEXT = 'UPDATE_TEXT'
const RESET_FORM = 'RESET_FORM'
const getInitialFormState = () => ({
text: '',
error: '',
touched: false
})
const formReducer = (state, action) => {
switch (action.type) {
case UPDATE_TEXT:
const text = action.text ?? ''
return {
...state,
text,
error: text.length < 6 ? 'Too short' : '',
touched: true
}
case RESET_FORM:
return getInitialFormState()
default:
return state
}
}
const Component = () => {
const [state, dispatch] = useReducer(formReducer, getInitialFormState())
const { text, error, touched } = state
const handleChange = (event) => {
const value = event.target.value
dispatch({ type: UPDATE_TEXT, text: value })
}
return <>
{!touched && <div>Write something...</div>}
<input type="text" value={text} onChange={handleChange} />
<div>Error: {error}</div>
</>
}
Почему так лучше
Централизация логики: Всё управление и переходы состояния находятся в одном
reducer
-функционале;Гибкость и масштабируемость: Легко добавлять новые состояния и действия — просто расширяйте
reducer
;Согласованное обновление: Состояния всегда обновляются атомарно, данные "не расходятся";
Упрощённый ререндер: Один
dispatch
запускает нужный переход всех частей состояния за раз.
Когда использовать useReducer
Если компонент хранит сложный объект состояния или массив и их части взаимосвязаны.
Когда переходы требуют логики, основанной на предыдущем состоянии.
Когда нужно легко расширять и модифицировать поведение компонента.
Нет жёсткого правила, когда переходить с useState
на useReducer
, но если у вас три и более взаимосвязанных состояния — пора подумать об useReducer
17. Пишите начальное состояние в виде функции, а не объекта
Проблема
В React
часто используют начальное состояние как объект.
Плохой пример
const initialFormState = {
text: '',
error: '',
touched: false
}
const formReducer = (state, action) => {
// Логика редьюсера
}
const Component = () => {
const [state, dispatch] = useReducer(formReducer, initialFormState)
// Остальной код
}
Однако такое объявление может привести к проблемам с мутабельностью. Если где-то в коде модифицировать объект initialFormState
, это приведёт к неожиданному поведению. Особенно это критично, если нужно сбрасывать состояние компонента к начальному или если используется один и тот же объект в разных местах (например, при тестах).
Возникающие проблемы
Мутация объекта: Если
initialFormState
изменится где-то в приложении, вы больше не получите “чистое” начальное состояние при его повторном применении;Ошибки при сбросе: Попытка сбросить форму к начальному состоянию приведёт к тому, что вы получите уже изменённый объект вместо оригинального;
Непредсказуемость в тестах: Множество юнит-тестов могут модифицировать это состояние, что приведёт к поломанным тестам при одновременном запуске.
Почему это плохо
Потеря чистоты: Вы не контролируете, что именно попадёт в начальное состояние при каждом новом его получении;
Скрытые баги: Возникают “плавающие” баги, которые сложно отследить — особенно в больших командах и при автоматизированных тестах.
Как стоит делать
Объявляйте начальное состояние как функцию, которая возвращает новый объект при каждом вызове:
const getInitialFormState = () => ({
text: '',
error: '',
touched: false
})
const formReducer = (state, action) => {
// Логика редьюсера
}
const Component = () => {
const [state, dispatch] = useReducer(formReducer, getInitialFormState())
// Остальной код
}
Почему так лучше
Всегда новый объект: Каждый вызов функции создаёт новый объект, изолированно и независимо от других экземпляров;
Безопасность: Нет риска случайно отредактировать общий объект и нарушить логику приложения;
Надёжные тесты: Юнит-тесты не влияют друг на друга и не ломаются при совместном запуске;
Правильный сброс: Можно безопасно сбросить форму или другое состояние, вызвав функцию заново.
Рекомендация
Объявляйте начальное состояние как функцию. Например:
const getInitialState = () =>; ({ ... })
Вызывайте функцию при инициализации
useReducer
или для сброса состояния;Так вы получите чистый, независимый объект каждый раз и избежите сложных ошибок.
18. Используйте useRef вместо useState, если не нужен повторный рендеринг компонента
Проблема
Многие используют useState
для хранения любых временных значений, даже если изменение этих значений не должно приводить к повторному рендеру компонента.
Плохой пример
import { useEffect, useState } from 'react'
const Component = () => {
const [triggered, setTriggered] = useState(false)
useEffect(() => {
if (!triggered) {
setTriggered(true)
// Какой-то код, который нужно выполнить один раз...
}
}, [triggered])
}
В этом примере при вызове setTriggered
происходит рендеринг компонента, хотя такие переменные, как "triggered
", часто используются только для отслеживания факта вызова действия и не влияют на отображение компонента.
Возникающие проблемы
Лишние рендеры: Каждый вызов
setTriggered
приводит к повторному рендеру, хотя это не влияет на результатJSX
;Неоптимальное поведение: Лишние обновления компонентов могут замедлить приложение, особенно при вложенных структурах;
Ошибки в логике: Такой паттерн не всегда работает так, как ожидается, например, попытка ограничить эффект одним вызовом при помощи
useState
часто не даёт желаемого результата в современных версияхReact
.
Почему это плохо
Потери производительности: Повторные рендеры без необходимости нагружают
React
и могут замедлить крупные приложения;Неочевидная логика: Использование
useState
ради флага, который не влияет на отображение, запутывает логику компонента и усложняет понимание кода.
Как стоит делать
Для хранения значений, которые не используются в JSX
и не должны вызывать ререндер, используйте useRef
. Его изменение не приводит к обновлению компонента:
import { useRef, useEffect } from 'react'
const Component = () => {
const triggeredRef = useRef(false)
useEffect(() => {
if (!triggeredRef.current) {
triggeredRef.current = true
// Какой-то однократный код…
}
}, [])
}
Почему так лучше
Нет лишних рендеров: Изменение
triggeredRef.current
не вызывает повторный рендер;Проще контролировать логику: Значение
useRef
не сбрасывается между рендерами и "привязано" к жизненному циклу компонента;Безопасное хранение флагов:
useRef
- лучший выбор для хранения любых "служебных" данных, которые не нужны для отображения, но должны переживать рендеры.
Почему нельзя использовать обычную внешнюю переменную?
// Такой код не будет работать ожидаемым образом!
let triggered = false
const Component = () => {
useEffect(() => {
if (!triggered) {
triggered = true
// Какой-то код...
}
}, [])
}
В этом случае переменная будет общей для всех экземпляров компонента и не привязана к его жизненному циклу. Если компонент размонтировать и снова примонтировать, значение не сбросится, что приведёт к ошибкам.
Рекомендация
Для любых флагов и значений, не влияющих на визуальное отображение, отдавайте предпочтение
useRef
:const myRef = useRef(initialValue)
ИзменяяmyRef.current
, вы не вызовете лишний рендер, но сохраните актуальное значение на весь жизненный цикл компонента.
19. Использование props или context в качестве начального состояния
Проблема
Распространённая ошибка — использовать значения из props
или context
при инициализации состояния через useState
или useRef
.
Плохой пример
const ArticleContent: React.FC<{ article: Articles["articles"]["0"] }> = (props) => {
// Антипаттерн: инициализация состояния на основе props
const [length] = useState<number>(
props.article.text.length + props.article.title.length
);
// Получение эмоций
const emotions = useGetEmoji();
// Эмоция, которая будет показана в UI
const [emotion, setEmotion] = useState<string>("");
useEffect(() => {
if (emotions) {
setEmotion(emotions["stickers"][length]);
}
}, [emotions, length]);
return (
<div>
<h2>{props.article.title}</h2>
<div>{props.article.text}</div>
<h3 dangerouslySetInnerHTML={{
__html: `Total Length ${length} ${emotion}`
}} />
</div>
);
};
Возникающие проблемы
Значение вычисляется только один раз — при инициализации компонента;
Если изменится внешний prop (например, выбранная статья),
length
не обновится;Приводит к устаревшему или некорректному состоянию
UI
;Побочные эффекты и вычисления, зависящие от
props/context
, не отработают после первого рендера.
Почему это плохо
“Застывшие” данные: Состояние не отражает актуальные props после их обновления, из-за чего
UI
и данные рассинхронизируются;Пропущенные эффекты:
React
не знает, что нужно пересчитать новое состояние при изменении пропсов, сетевые запросы или другие логики не выполняются;Дублирование источников правды: Возникает два источника одних и тех же данных — props и состояние. Это может привести к трудноуловимым багам и ошибкам синхронизации.
Как стоит делать
Используйте
props
напрямую для вычислений, если значение на их основе требуется всегда актуальное;Если данные зависят от
props
, производите вычисления в теле функции (или используйтеuseMemo
/useEffect
с актуальными зависимостями):
const ArticleContent: React.FC<{ article: Articles["articles"]["0"] }> = (props) => {
// Правильно: вычисляем значение прямо на рендере
const length = props.article.text.length + props.article.title.length;
const emotions = useGetEmoji();
const [emotion, setEmotion] = useState<string>("");
useEffect(() => {
if (emotions) {
setEmotion(emotions["stickers"][length]);
}
}, [emotions, length]);
return (
<div>
<h2>{props.article.title}</h2>
<div>{props.article.text}</div>
<h3 dangerouslySetInnerHTML={{
__html: `Total Length ${length} ${emotion}`
}} />
</div>
);
};
Если нужно синхронизировать состояние с
props
, контролируйте это с помощьюuseEffect
:
const [length, setLength] = useState(props.article.text.length + props.article.title.length);
useEffect(() => {
setLength(props.article.text.length + props.article.title.length);
}, [props.article]);
Почему так лучше
React всегда использует свежие данные из
props
;Вы не дублируете данные и не ломаете принцип единого источника правды;
Все эффекты и вычисления выполняются корректно при каждом изменении пропсов или контекста.
20. “Удаление и восстановление” (Remount on Prop Change)
Проблема
В попытке решить проблему с устаревшими вычислениями при смене props
, многие разработчики прибегают к следующему “решению”: заставить React полностью удалять и пересоздавать компонент при изменении props
, используя свойство key
. Такой подход зачастую маскирует ошибку в проектировании состояния, но приводит к новым побочным эффектам.
Плохой пример
const PartiallyCorrect: React.FC = () => {
const articles = useGetArticles();
const [currentArticle, setCurrentArticle] = useState<
Articles["articles"]["0"] | null
>();
const onClickHandler = useCallback((article) => {
setCurrentArticle(article);
}, []);
return (
<div style={styles.container}>
<Navigation articles={articles} onClickHandler={onClickHandler} />
<div style={styles.content}>
{currentArticle ? (
// Антипаттерн: удаление и восстановление по key
<ArticleContent article={currentArticle} key={currentArticle.id} />
) : null}
</div>
</div>
);
};
Возникающие проблемы
Мерцание: При каждом изменении ключа компонент полностью удаляется из
DOM
и создаётся заново, из-за чего пользователь видит неприятный визуальный эффект (размонтирование и монтирование);Потеря состояния: Все локальные состояния и хуки обнуляются, даже если часть данных была жизненно важной для
UX
;Сброс эффектов: Сторонние запросы и эффекты будут вызываться заново инициализироваться, что увеличивает ненужную нагрузку на бэкенд и может вызывать задержки;
Ненужные перерисовки: Компонент пересоздаётся даже там, где можно было обойтись простым пересчётом значения.
Почему это плохо
Плохая производительность: Частые полные размонтирования и монтирования компонентов — тяжёлое действие для
React
;Плохо для пользователя: Особенно заметно при медленных сетевых запросах или если потомок содержит анимации, временные данные и т.д;
Обход проблемы, а не её решение: Вместо корректной синхронизации состояния с
props
вы вводите искусственное пересоздание компонента.
Как стоит делать
Не прибегайте к трюкам с
key
, чтобы “перемонтировать” компонент. Вместо этого корректно управляйте состояниями, зависящими отprops
;Если значение всегда вычисляется на лету из актуальных
props
— храните только идентификаторы, вычисляйте производные значения/вызывайте эффекты черезuseEffect
;Используйте
useEffect
для отслеживания измененийprops
и синхронизации состояния, если это действительно нужно.
import { useEffect, useState } from "react";
import { useGetArticles } from "../hooks/useGetArticles";
import { useGetEmoji } from "../hooks/useGetEmoji";
import { Navigation } from "../components/Navigation";
import { Articles } from "../types";
const styles = {
container: {
background: "#FEF2F2",
height: "100%",
display: "grid",
gridTemplateColumns: "10rem auto"
},
content: {}
};
const ArticleContent = ({ article }) => {
// Длина текста и заголовка всегда актуальна для статьи
const length = article.text.length + article.title.length;
// Актуальные данные о стикерах (эмоциях)
const emotions = useGetEmoji();
// Храним эмоцию по индексу длины — она пересчитывается при смене статьи или эмоций
const [emotion, setEmotion] = useState("");
useEffect(() => {
if (emotions) {
setEmotion(emotions["stickers"][length]);
}
}, [emotions, length]); // Следим за обновлениями: если поменялась статья или эмоции
return (
<div>
<div>
<h2>{article.title}</h2>
<div>{article.text}</div>
</div>
<h3
dangerouslySetInnerHTML={{
__html: `Total Length ${length} ${emotion}`
}}
/>
</div>
);
};
const Correct = () => {
const articles = useGetArticles();
const [currentArticle, setCurrentArticle] = useState(null);
const onClickHandler = (article) => setCurrentArticle(article);
return (
<div style={styles.container}>
<Navigation articles={articles} onClickHandler={onClickHandler} />
<div style={styles.content}>
{currentArticle ? <ArticleContent article={currentArticle} /> : null}
</div>
</div>
);
};
export default Correct;
Почему так лучше
Нет лишних пересозданий и мерцаний; компонент обновляет только нужные части состояния;
Ваш код становится проще, понятнее, предсказуемее;
Производительность приложения выше, пользовательский опыт — лучше.
Изложенные подходы — профессиональные рекомендации и отправная точка для инженерных решений, а не нормативное руководство. Выбор конкретной тактики зависит от контекста: требований продукта, ограничений платформы, SLA, профиля производительности и договоренностей в команде. Рассматривайте эти практики как чек-лист для проектирования и ревью; финальные решения принимайте на основе измерений и компромиссов, релевантных вашей системе.
gsaw
Мне кажется или 1 и 2 находятся в противоречии друг к другу. Если пофиксить 1 паттерн, то получается исходник ко второму паттерну, если второй пофиксить как предлагается, то получается 1 паттерн.
Dakirchik Автор
1-й пункт:
«Не храни в стейте то, что можно вычислить на лету. Лишние рендеры никому не нужны.»
2-й пункт:
«Если данные должны сохраняться между рендерами (например, состояние формы), обычные переменные не подойдут — только
useState
илиuseRef
.»Где противоречие?
Первый случай — данные уже есть (
props
), просто комбинируем их.Второй случай — данные динамические, и компонент должен их «помнить».
Получается:
Производные значения → вычисляй прямо в компоненте.
Настоящее состояние →
useState
.Всё, вроде бы, логично, просто контексты разные))
gsaw
Просто мне непонятно, как можно вычислить someValue без каких то внешних данных. Либо это константа, то ее вынести вовне, либо это вычисляемое из пропсов, что похоже на первый паттерн. Либо вот в стейте хранить.
Dakirchik Автор
А, ну тут всё просто на самом деле. Давай на примерах, чтобы стало понятно, где что применять:
1. Константа (вообще не зависит ни от чего)
jsx
2. Вычисляемое из пропсов (как в 1-м паттерне)
jsx
3. Состояние (когда нужно "помнить" между рендерами)
jsx
Где твой
someValue
?Если
someValue
:Ниоткуда не берётся (константа) → вариант 1
Считается из пропсов → вариант 2
Меняется внутри компонента и должен сохраняться → вариант 3
Получается: если значение можно получить прямо сейчас из того, что уже есть (пропсы/другие константы) — не пихай его в стейт. Стейт нужен только для того, что компонент должен "помнить" сам.
Надеюсь подробно описал что да как)