Шаг 4. Ссылки и оптимизация (useRef, useCallback, memo)
Приложение растёт, список длинный — пора подумать о рендерах. На этом шаге: автофокус на поле ввода через useRef, подсветка изменения количества через usePrevious, и оптимизация строк списка через React.memo + useCallback.
Постановка задачи
Заголовок раздела «Постановка задачи»- После добавления задачи поле ввода должно сохранять фокус (через
useRefна input); - покажи, на сколько изменилось число задач с прошлого рендера (хук
usePrevious, построенный наuseRef); - вынеси строку задачи в отдельный компонент
TaskItem, оберни его вReact.memo; - обработчики
onToggle/onRemove, передаваемые вTaskItem, оберни вuseCallback, чтобыmemoреально работал и строки не перерисовывались при каждом рендере родителя.
Какие API и зачем
Заголовок раздела «Какие API и зачем»useRef<HTMLInputElement>(null)— ссылка на DOM-узел. Меняем фокус императивно (inputRef.current?.focus()) — это не состояние, ре-рендер не нужен.useRefдля мутабельного значения —usePreviousхранит предыдущее значение между рендерами без ре-рендера: записываем вref.currentвнутриuseEffect, возвращаем то, что было до обновления.React.memo(Component)— пропускает ре-рендер компонента, если пропсы не изменились по ссылке. Нужен для дорогих/многочисленных строк списка.useCallback(fn, deps)— стабилизирует ссылку на функцию между рендерами. Без него каждый рендер родителя создаёт новыйonToggle, иReact.memoуTaskItemбесполезен (пропс-функция всегда «новая»).
Показать эталонную реализацию
import { memo, useCallback, useEffect, useRef, useState } from 'react';import type { Task } from '../types/task';
// usePrevious — хранит предыдущее значение через useRef (без ре-рендера)function usePrevious<T>(value: T): T | undefined { const ref = useRef<T>(); useEffect(() => { ref.current = value; }, [value]); return ref.current;}
// Строка задачи — мемоизирована: ре-рендер только если изменились её пропсыinterface ItemProps { task: Task; onToggle: (id: string) => void; onRemove: (id: string) => void;}
const TaskItem = memo(function TaskItem({ task, onToggle, onRemove }: ItemProps) { return ( <li> <input type="checkbox" checked={task.done} onChange={() => onToggle(task.id)} /> <span style={{ textDecoration: task.done ? 'line-through' : 'none' }}> {task.title} </span> <button onClick={() => onRemove(task.id)}>×</button> </li> );});
export function TaskApp() { const [tasks, setTasks] = useState<Task[]>([]); const [draft, setDraft] = useState(''); const inputRef = useRef<HTMLInputElement>(null); // ссылка на input
const prevCount = usePrevious(tasks.length); const delta = prevCount === undefined ? 0 : tasks.length - prevCount;
// useCallback: стабильные ссылки → React.memo у TaskItem работает const addTask = useCallback((title: string) => { const trimmed = title.trim(); if (!trimmed) return; setTasks((prev) => [ ...prev, { id: crypto.randomUUID(), title: trimmed, done: false, createdAt: Date.now() }, ]); setDraft(''); inputRef.current?.focus(); // вернуть фокус после добавления }, []);
const toggleTask = useCallback((id: string) => { setTasks((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))); }, []);
const removeTask = useCallback((id: string) => { setTasks((prev) => prev.filter((t) => t.id !== id)); }, []);
return ( <section> <form onSubmit={(e) => { e.preventDefault(); addTask(draft); }} > <input ref={inputRef} value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Новая задача..." /> <button type="submit">Добавить</button> </form>
{delta !== 0 && <small>Изменение: {delta > 0 ? `+${delta}` : delta}</small>}
<ul> {tasks.map((task) => ( <TaskItem key={task.id} task={task} onToggle={toggleTask} onRemove={removeTask} /> ))} </ul> </section> );}Ловушки
Заголовок раздела «Ловушки»Дальше — Шаг 5: сложное состояние через useReducer и useContext.