render mounts a component into a container appended to document.body; screen is a set of queries bound to that body. Cleanup is automatic, because both runners expose a global afterEach. There is no wrapper object, no instance, no way to reach into state — by design.
Start with a component that has a bug: Counter renders <p role="status">Clicked {count} times</p> and a button whose handler is () => setCount(c => c + 1), ignoring the step prop it accepts.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
test('starts at zero', () => {
render(<Counter />);
expect(screen.getByRole('status')).toHaveTextContent('Clicked 0 times');
});
test('adds step on every click', async () => {
const user = userEvent.setup();
render(<Counter step={5} />);
const button = screen.getByRole('button', { name: /click me/i });
await user.click(button);
await user.click(button);
expect(screen.getByRole('status').textContent).toBe('Clicked 10 times');
});Neither test mentions useState or the number of renders — only a status message, a button with a visible label, and a number the user can read:
✓ src/Counter.test.jsx > starts at zero 122ms × src/Counter.test.jsx > adds step on every click 138ms FAIL src/Counter.test.jsx > adds step on every click AssertionError: expected 'Clicked 2 times' to be 'Clicked 10 times' // Object.is equality Expected: "Clicked 10 times" Received: "Clicked 2 times" ❯ src/Counter.test.jsx:16:50
Two clicks produced 2, not 10: step is ignored. Change the handler to setCount(c => c + step) and both tests pass.
render also returns an object for the cases screen cannot cover: container is the wrapping <div>, used for snapshots; rerender(<Counter step={2} />) re-renders the same instance with new props; unmount() tears the tree down so you can assert that a cleanup ran; debug() pretty-prints the markup; wrapper renders children inside every provider the tree expects.