Тестируй поведение, а не реализацию, используй двойные вызовы StrictMode и измеряй рендеры через Profiler.
Открыть этот урок в KodokonФилософия Testing Library умещается в одну фразу: тестируй то, что воспринимает пользователь, и никогда - детали реализации. На практике это значит запрашивать DOM через дерево доступности: сначала getByRole (королева запросов, попутно проверяющая твои ARIA-роли), затем getByLabelText, getByText, и только в самом крайнем случае getByTestId. Для взаимодействий предпочитай user-event вместо fireEvent: там, где fireEvent.change порождает одно изолированное событие, user.type воспроизводит всю последовательность (focus, keydown, keypress, input, keyup), ровно как настоящая клавиатура. Варианты findBy* соединяют запрос с асинхронным ожиданием: незаменимо после шага загрузки.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('adds a task to the list', async () => {
const user = userEvent.setup();
render(<TodoApp />);
await user.type(
screen.getByRole('textbox'),
'Review the report'
);
await user.click(
screen.getByRole('button', { name: /add/i })
);
expect(
await screen.findByText('Review the report')
).toBeInTheDocument();
});StrictMode - инструмент разработки, не имеющий ровно никакого эффекта в продакшене. В разработке он дважды вызывает функции твоих компонентов, инициализаторы состояния и функции, переданные в setState, чтобы вытащить наружу нечистые рендеры: если два выполнения дают разные результаты, у тебя есть скрытый побочный эффект. Начиная с React 18 он также прогоняет каждый эффект через цикл монтирование, размонтирование, повторное монтирование: эффект, чья очистка не идеально симметрична (подписка, которую не отменили, запрос, который не прервали), выдаёт себя сразу же. Это прямая подготовка к возможностям, где React размонтирует, а затем снова монтирует экран, сохраняя его состояние.
useEffect(() => {
const controller = new AbortController();
fetch('/api/user', { signal: controller.signal })
.then((res) => res.json())
.then(setUser)
.catch(() => {});
return () => controller.abort();
}, []);Для измерений React даёт компонент <Profiler> и его графический аналог в DevTools. Его коллбэк onRender получает, помимо прочего, phase (mount, update или nested-update), actualDuration (время, реально потраченное на рендеринг поддерева в этом коммите) и baseDuration (оценку времени рендеринга всего поддерева без какой-либо мемоизации). Разрыв между ними измеряет эффективность твоих memo и useMemo: actualDuration, сильно меньший baseDuration, означает, что мемоизация работает на тебя.
import { Profiler } from 'react';
function App() {
return (
<Profiler
id="Sidebar"
onRender={(id, phase, actualDuration) => {
console.log(id, phase, actualDuration);
}}
>
<Sidebar />
</Profiler>
);
}baseDuration в коллбэке Profiler?