В React хук useEffect используется для управления
побочными эффектами, такими как запросы к API, подписки, таймеры и
изменение DOM вне реактивного цикла. Тестирование компонентов с
useEffect требует понимания того, как этот хук реагирует на
зависимости и как асинхронные операции влияют на
рендер.
useEffectХук useEffect принимает два аргумента:
useEffect(() => {
// побочный эффект
return () => {
// очистка эффекта (необязательная)
};
}, [dependencies]);
Функция эффекта выполняется после рендера компонента.
Массив зависимостей определяет, когда эффект будет повторно вызываться.
[]) → эффект выполняется один раз при
монтировании.[dep1, dep2]) → эффект
вызывается при изменении любой зависимости.При тестировании важно учитывать, что асинхронные
эффекты могут завершаться позже, чем основной рендер, и это
необходимо обрабатывать через waitFor, findBy
и другие утилиты React Testing Library.
Пример компонента, который делает один запрос к API при монтировании:
import { useEffect, useState } from 'react';
export function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
if (!user) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
Тестирование с помощью React Testing Library:
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from './UserProfile';
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'Alice' }),
})
);
test('загружает и отображает пользователя', async () => {
render(<UserProfile userId={1} />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => expect(screen.getByText('Alice')).toBeInTheDocument());
});
Ключевые моменты:
fetch, чтобы не
обращаться к реальному API.waitFor, который
ждёт изменения DOM.Alice), а не через внутренние вызовы хука.Если массив зависимостей содержит значения, тест должен проверять поведение при их изменении.
Пример компонента, реагирующего на изменение userId:
export function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
setUser(null);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
if (!user) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
Тестирование изменения зависимости:
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from './UserProfile';
import { act } from 'react-dom/test-utils';
test('обновляет данные при изменении userId', async () => {
const { rerender } = render(<UserProfile userId={1} />);
await waitFor(() => expect(screen.getByText('Alice')).toBeInTheDocument());
global.fetch.mockImplementationOnce(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'Bob' }),
})
);
rerender(<UserProfile userId={2} />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => expect(screen.getByText('Bob')).toBeInTheDocument());
});
Особенности:
rerender, чтобы симулировать изменение
пропсов.fetch.Если эффект возвращает функцию очистки (cleanup), важно
убедиться, что она вызывается при размонтировании или при изменении
зависимостей.
Пример с подпиской:
import { useEffect, useState } from 'react';
export function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => setSeconds(s => s + 1), 1000);
return () => clearInterval(id);
}, []);
return <div>{seconds}</div>;
}
Тест с таймерами:
import { render, screen } from '@testing-library/react';
import { Timer } from './Timer';
import { act } from 'react-dom/test-utils';
jest.useFakeTimers();
test('таймер увеличивает секунды', () => {
render(<Timer />);
expect(screen.getByText('0')).toBeInTheDocument();
act(() => {
jest.advanceTimersByTime(1000);
});
expect(screen.getByText('1')).toBeInTheDocument();
act(() => {
jest.advanceTimersByTime(2000);
});
expect(screen.getByText('3')).toBeInTheDocument();
});
Особенности:
jest.useFakeTimers(), чтобы управлять
временем в тестах.act() оборачивает изменения состояния, связанные с
таймерами, чтобы правильно отразить их в DOM.useEffect на конкретные измененияВ сложных компонентах важно тестировать, что эффект не вызывается лишний раз, если зависимости не изменились.
const effectFn = jest.fn();
function Component({ value }) {
useEffect(() => {
effectFn();
}, [value]);
return <div>{value}</div>;
}
test('эффект вызывается только при изменении value', () => {
const { rerender } = render(<Component value={1} />);
expect(effectFn).toHaveBeenCalledTimes(1);
rerender(<Component value={1} />);
expect(effectFn).toHaveBeenCalledTimes(1);
rerender(<Component value={2} />);
expect(effectFn).toHaveBeenCalledTimes(2);
});
Ключевые моменты:
jest.fn().rerender для имитации изменений
зависимостей.Для асинхронных эффектов важно обрабатывать возможные ошибки, чтобы тесты не зависали:
useEffect(() => {
let mounted = true;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (mounted) setUser(data);
})
.catch(err => console.error(err));
return () => { mounted = false; };
}, [userId]);
В тестах проверка обработки ошибок:
global.fetch.mockImplementationOnce(() => Promise.reject('API Error'));
render(<UserProfile userId={3} />);
await waitFor(() => expect(screen.getByText(/loading/i)).toBeInTheDocument());
Это позволяет предотвратить обновление состояния после размонтирования компонента.
useEffectwaitFor,
findBy, act().rerender и
проверять конечное состояние.jest.fn().Тщательная проверка useEffect и его зависимостей
обеспечивает корректное поведение компонентов и минимизирует баги,
связанные с асинхронными операциями и изменениями пропсов. Правильное
использование инструментов React Testing Library позволяет тестировать
эффекты реалистично и безопасно, отражая реальное
поведение интерфейса.