Доки по разработке
This project is maintained by teniryte
В этом разделе собраны ключевые паттерны работы с наиболее распространёнными хуками React: useState, useEffect, useLayoutEffect, useId, а также приёмы очистки ресурсов и использования callback‑refs.
useState)Контролируемый компонент — это элемент формы, значение которого полностью определяется состоянием React. Источник правды один: state компонента, а DOM только отображает это значение.
Паттерн:
useState;value;onChange.export function ControlledInput() {
const [name, setName] = useState('');
return (
<div>
<label>
Name:
<input
value={name}
onChange={event => setName(event.target.value)}
/>
</label>
<p>Текущее значение: {name || '—'}</p>
</div>
);
}
Если нужно показать значение, но запретить редактирование, добавьте readOnly — иначе React будет ругаться на “контролируемый компонент без обработчика”.
Derived state — это значения, которые можно вычислить на основе других частей состояния, а не хранить отдельно. Хранение дублирующих данных усложняет код и может привести к рассинхронизации.
Пример: строка query содержит перечисление цветов, а чекбоксы “зеркалят” её.
const COLORS = ['red', 'green', 'blue', 'yellow'] as const;
type Color = (typeof COLORS)[number];
export function ColorFilter() {
const [query, setQuery] = useState('');
// Производное состояние: отмеченные цвета
const selected: Record<Color, boolean> = COLORS.reduce(
(acc, color) => ({
...acc,
[color]: query.includes(color),
}),
{} as Record<Color, boolean>,
);
const toggleColor = (color: Color) => {
setQuery(prev => {
if (prev.includes(color)) {
return prev.replace(color, '').trim();
}
return `${prev} ${color}`.trim();
});
};
return (
<div>
<input
placeholder="Введите цвета: red green ..."
value={query}
onChange={e => setQuery(e.target.value)}
/>
{COLORS.map(color => (
<label key={color}>
<input
type="checkbox"
checked={selected[color]}
onChange={() => toggleColor(color)}
/>
{color}
</label>
))}
</div>
);
}
Правило: если значение можно получить из другого значения чистой функцией, лучше вычислить его “на лету”, а не хранить в отдельном useState.
useState(initialValue) вычисляет initialValue при каждом рендере. Если вычисление тяжёлое, лучше передать функцию — тогда она выполнится только один раз при первом рендере.
function createInitialValue() {
console.log('Вычисляем начальное значение');
// тут может быть чтение из localStorage, парсинг JSON и т.п.
return 42;
}
export function LazyStateExample() {
// функция вызывается только один раз
const [value, setValue] = useState(() => createInitialValue());
return (
<div>
<p>Значение: {value}</p>
<button onClick={() => setValue(v => v + 1)}>Увеличить</button>
</div>
);
}
Используйте ленивую инициализацию, когда:
useLayoutEffect и useEffectОба хука используются для побочных эффектов, но вызываются в разное время:
useEffect запускается после того, как изменения уже отрисованы на экране;useLayoutEffect срабатывает сразу после рендера, но до отрисовки — браузер пока “держит” кадр.Обычно достаточно useEffect. useLayoutEffect нужен для случаев, когда:
export function EffectComparison() {
const [count, setCount] = useState(0);
useLayoutEffect(() => {
console.log('useLayoutEffect: отработал перед отрисовкой');
}, [count]);
useEffect(() => {
console.log('useEffect: отработал после отрисовки');
}, [count]);
return (
<button onClick={() => setCount(c => c + 1)}>
Нажато: {count}
</button>
);
}
Эффект, который создаёт ресурсы (таймеры, подписки, глобальные структуры данных), должен возвращать функцию очистки. Она будет вызвана:
export function IntervalExample() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Очистка
return () => {
clearInterval(id);
};
}, []);
return <p>Прошло секунд: {seconds}</p>;
}
Тот же принцип применим и к любым “тяжёлым” ресурсам — WebSocket, слушателям DOM, глобальным массивам и т.д.
useId и стабильные идентификаторыuseId генерирует уникальные идентификаторы, которые совпадают между сервером и клиентом. Это важно для доступности и корректной гидратации.
Типичный сценарий — связка label и input, а также ARIA‑атрибуты:
export function AccessibleInput() {
const id = useId();
return (
<div>
<label htmlFor={id}>Email</label>
<input id={id} name="email" type="email" />
</div>
);
}
Если таких полей много, useId гарантирует, что каждый ID уникален и не “прыгает” между рендерами.
Вместо объектного ref (const ref = useRef(null)) можно использовать callback‑ref — функцию, которая будет вызвана при монтировании и размонтировании элемента.
Это удобно, когда:
useRef.export function CallbackRefExample() {
const [visible, setVisible] = useState(true);
return (
<div>
{visible && (
<h1
ref={node => {
if (node) {
console.log('Элемент смонтирован', node);
} else {
console.log('Элемент размонтирован');
}
}}
>
Заголовок
</h1>
)}
<button onClick={() => setVisible(v => !v)}>
Показать / скрыть
</button>
</div>
);
}
Стоит один раз внимательно посмотреть на “hook‑flow” — диаграмму, показывающую порядок вызовов:
useLayoutEffect и его очистки;useEffect и его очистки.Понимание этой последовательности помогает: