Test behavior rather than implementation, leverage StrictMode's double invocations, and measure renders with Profiler.
Open this lesson in KodokonThe philosophy of Testing Library fits in one sentence: test what the user perceives, never the implementation details. Concretely, you query the DOM through the accessibility tree: getByRole first (the queen of queries, which validates your ARIA roles along the way), then getByLabelText, getByText, and getByTestId only as a last resort. For interactions, prefer user-event over fireEvent: where fireEvent.change emits an isolated event, user.type reproduces the full sequence (focus, keydown, keypress, input, keyup), exactly like a real keyboard. The findBy* variants combine a query with an async wait: essential after a loading step.
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 is a development tool with no effect whatsoever in production. In development, it calls your component functions, state initializers, and functions passed to setState twice, to flush out impure renders: if two executions give different results, you have a hidden side effect. Since React 18, it also runs each effect through the mount, unmount, remount cycle: an effect whose cleanup is not perfectly symmetrical (subscription never cancelled, request never aborted) betrays itself immediately. It is direct preparation for features where React unmounts then remounts a screen while preserving its state.
useEffect(() => {
const controller = new AbortController();
fetch('/api/user', { signal: controller.signal })
.then((res) => res.json())
.then(setUser)
.catch(() => {});
return () => controller.abort();
}, []);To measure, React provides the <Profiler> component and its graphical counterpart in the DevTools. Its onRender callback receives, among other things, phase (mount, update, or nested-update), actualDuration (the time actually spent rendering the subtree for this commit), and baseDuration (an estimate of the time to render the entire subtree without any memoization). The gap between the two measures the effectiveness of your memo and useMemo: an actualDuration far below baseDuration means memoization is working for you.
import { Profiler } from 'react';
function App() {
return (
<Profiler
id="Sidebar"
onRender={(id, phase, actualDuration) => {
console.log(id, phase, actualDuration);
}}
>
<Sidebar />
</Profiler>
);
}baseDuration represent in the Profiler callback?