act для обновления hook состояния

act — это вспомогательный механизм из React, предназначенный для корректной обработки обновлений состояния и эффектов в тестовой среде. Его основная задача — гарантировать, что все изменения состояния, вызванные пользовательскими действиями, асинхронными операциями или жизненным циклом компонентов, будут полностью применены до момента выполнения проверок.

В контексте React Testing Library act играет ключевую роль при тестировании хуков и компонентов, зависящих от обновления состояния. Без него тесты могут становиться нестабильными, выдавать предупреждения или работать некорректно из-за незавершённых обновлений.


Внутренний принцип работы act

React использует пакетную обработку обновлений состояния. Это означает, что вызовы setState, обновления через useState, useReducer или побочные эффекты (useEffect) не всегда выполняются синхронно. В обычном приложении React сам управляет очередью обновлений, но в тестах эта очередь должна быть явно завершена.

act создаёт специальный контекст, внутри которого React:

  • отслеживает все вызовы обновления состояния;
  • выполняет синхронные и асинхронные эффекты;
  • завершает фазу рендера и коммита перед выходом из act.

Только после этого результаты можно считать стабильными и пригодными для проверок.


Базовое использование act с хуками

При тестировании хуков напрямую (например, через renderHook) любое изменение состояния должно быть обёрнуто в act.

import { renderHook, act } from '@testing-library/react';

test('обновление состояния useState', () => {
  const { result } = renderHook(() => {
    const [count, setCount] = React.useState(0);
    return { count, setCount };
  });

  act(() => {
    result.current.setCount(1);
  });

  expect(result.current.count).toBe(1);
});

Ключевые моменты:

  • все вызовы, которые могут привести к изменению состояния, выполняются внутри act;
  • проверки выполняются только после выхода из act;
  • вложенные обновления также учитываются автоматически.

Асинхронный act и обновления хуков

Хуки часто используют асинхронные операции: таймеры, промисы, запросы данных. Для таких случаев применяется асинхронная версия act.

test('асинхронное обновление состояния', async () => {
  const { result } = renderHook(() => {
    const [value, setValue] = React.useState(null);

    React.useEffect(() => {
      Promise.resolve('data').then(setValue);
    }, []);

    return value;
  });

  await act(async () => {
    await Promise.resolve();
  });

  expect(result.current).toBe('data');
});

Особенности асинхронного act:

  • функция внутри act возвращает Promise;
  • await act(...) гарантирует завершение всех асинхронных эффектов;
  • React дожидается выполнения useEffect и связанных обновлений.

Предупреждение React и его причины

Типичное сообщение:

Warning: An update to ComponentName inside a test was not wrapped in act(...)

Это предупреждение означает, что React обнаружил обновление состояния вне контролируемого контекста. Причины:

  • вызов setState вне act;
  • асинхронный эффект, не дождавшийся завершения;
  • обновление состояния после выполнения проверок.

Игнорирование предупреждения может привести к:

  • флапающим тестам;
  • ложноположительным результатам;
  • несоответствию поведения тестов реальному приложению.

Связь act и пользовательских событий

React Testing Library стремится абстрагировать act от разработчика. Большинство утилит уже используют его внутри себя.

Пример:

fireEvent.click(button);

или

await userEvent.click(button);

В этих случаях act вызывается автоматически. Однако это работает только для событий, инициированных через API библиотеки. Любая ручная логика, влияющая на состояние, по-прежнему требует явного act.


act и таймеры

При использовании таймеров (setTimeout, setInterval) необходимо сочетать act с управлением временем.

jest.useFakeTimers();

test('обновление после таймера', () => {
  const { result } = renderHook(() => {
    const [ready, setReady] = React.useState(false);

    React.useEffect(() => {
      setTimeout(() => setReady(true), 1000);
    }, []);

    return ready;
  });

  act(() => {
    jest.runAllTimers();
  });

  expect(result.current).toBe(true);
});

Важно:

  • управление таймерами должно происходить внутри act;
  • выполнение таймеров инициирует обновления состояния, которые React должен обработать полностью.

Вложенные обновления и act

React допускает цепочки обновлений, когда одно изменение состояния вызывает другое. act корректно обрабатывает такие сценарии.

act(() => {
  result.current.increment();
  result.current.increment();
});

Все изменения будут объединены в один стабильный результат. Это соответствует поведению React в реальном приложении.


Отличие act от waitFor

waitFor используется для ожидания определённого состояния, а не для инициирования обновлений.

  • act — управляет процессом обновления
  • waitFor — ожидает результат обновления

Часто они используются совместно, но не взаимозаменяемы. waitFor не заменяет необходимость оборачивать обновления в act.


Ошибки неправильного применения

Распространённые проблемы:

  • использование act только вокруг ассертов;
  • ожидание, что act автоматически выполнит асинхронную логику без await;
  • оборачивание render вместо логики, вызывающей обновления.

Корректный подход:

  • render выполняется отдельно;
  • act оборачивает только действия, меняющие состояние;
  • проверки выполняются после полного завершения act.

Поведение в React 18

В React 18 усилилась автоматическая пакетная обработка обновлений, но требования к act остались. В строгом режиме (StrictMode) эффекты могут вызываться дважды, что делает корректное использование act особенно важным при тестировании хуков с побочными эффектами.


Практическая роль act при тестировании хуков

act — это не просто техническое требование, а инструмент синхронизации тестов с внутренней моделью React. Он обеспечивает:

  • детерминированность тестов;
  • соответствие поведения тестов реальному приложению;
  • корректную работу с хуками, эффектами и асинхронностью.

При тестировании hook-логики act становится фундаментальным элементом, без которого невозможно получить надёжный и предсказуемый результат.