Dev Highlights

Доки по разработке

This project is maintained by teniryte

Хуки React

В этом разделе собраны ключевые паттерны работы с наиболее распространёнными хуками React: useState, useEffect, useLayoutEffect, useId, а также приёмы очистки ресурсов и использования callback‑refs.

Контролируемые компоненты (useState)

Контролируемый компонент — это элемент формы, значение которого полностью определяется состоянием React. Источник правды один: state компонента, а DOM только отображает это значение.

Паттерн:

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)

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 нужен для случаев, когда:

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 уникален и не “прыгает” между рендерами.

Callback refs

Вместо объектного ref (const ref = useRef(null)) можно использовать callback‑ref — функцию, которая будет вызвана при монтировании и размонтировании элемента.

Это удобно, когда:

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” — диаграмму, показывающую порядок вызовов:

Понимание этой последовательности помогает: