react-shallow-renderer
A DOM-free test renderer that executes one custom React component and returns the React element it produced without rendering child components. Tests can inspect the root element's type, props, and children, rerender with new props, or reach a mounted class instance. It supports much of the React 18-era component model, including function components, class components, memo, forwardRef, and state hooks, but deliberately skips refs, effects, and post-commit lifecycles. It is now a compatibility tool for React 16 through 18, not a current React testing default.
Keep it only where a React 16 through 18 test suite already depends on shallow semantics. Do not install it for React 19 or a new project; Testing Library catches behavior and integration failures that one-level element inspection cannot see.
Use it if
- You maintain an existing React 16, 17, or 18 suite whose assertions intentionally depend on one-level rendering
- You need fast structural tests for a pure component without jsdom, a native host environment, or child execution
- You are preserving a React Native test suite on a compatible React version and only inspect emitted element props
- You need direct access to a shallow-rendered class instance to support a gradual migration of older tests
- You use React 19: the published peer range ends at React 18, its source reads React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, and React 19 support remains an open request and pull request
- You want tests that reflect user behavior: child components never render, so context integration, effects, DOM or native events, accessibility, layout, and real update interactions can all be missed
- Your component relies on refs, useEffect, useLayoutEffect, or useInsertionEffect: the README says refs are unsupported, and the source implements all three effect hooks as no-ops
- You expect mount and update lifecycles: the source intentionally skips componentDidMount and componentDidUpdate because no DOM refs exist
- You are choosing a new test stack: the last release added React 18 support in April 2022, while current React guidance recommends Testing Library for modern component tests
Setup reality
Install it as a development dependency, but check React first. Version 16.15.0 declares react ^16.0.0, ^17.0.0, or ^18.0.0 as its peer and installs object-assign plus react-is; React 19 is outside that contract. There is no test runner, assertion library, JSX transform, DOM, or native renderer included, so Jest, Vitest, Babel, TypeScript, and source transforms remain your responsibility. The package publishes CommonJS, ESM, and UMD files, but package.json exposes only the CommonJS index and has no exports or module field. It also ships no TypeScript declarations. Basic use is synchronous: create a new ShallowRenderer, pass a custom component element to render, then inspect getRenderOutput. Passing a host element such as <div /> directly throws because the root must be a function, class, memo, or forwardRef component. Children in the returned output are still React elements, not mounted instances, and there are no find, query, simulate, debug, or accessibility helpers. To exercise a handler, call the relevant output prop yourself; that is not equivalent to browser or React Native event dispatch. Hook state and reducers are modeled, but effect hooks are no-ops, transition state is simplified, useSyncExternalStore only reads getSnapshot, and refs are unsupported. Class componentDidMount and componentDidUpdate do not run, although componentWillUnmount runs when renderer.unmount() is called. The implementation reaches into React secret internals and carries React 18-era symbol knowledge, which is why forcing installation beside React 19 is unsafe even if a package manager can be persuaded. Pin React 18 and the renderer together for legacy suites. For new tests, use @testing-library/react in web projects or @testing-library/react-native in native projects and assert visible behavior instead of component structure.
Patterns
Render one function componentrender-function-component
import ShallowRenderer from 'react-shallow-renderer';
function Greeting({ name }) {
return <h1>Hello, {name}</h1>;
}
const renderer = new ShallowRenderer();
renderer.render(<Greeting name="Ada" />);
const output = renderer.getRenderOutput();
expect(output.type).toBe('h1');
expect(output.props.children).toEqual(['Hello, ', 'Ada']);The root passed to render must be a custom component. Passing <h1> directly throws.
Inspect props on the emitted host elementassert-output-props
renderer.render(<Button disabled label="Save" />);
const output = renderer.getRenderOutput();
expect(output.type).toBe('button');
expect(output.props.disabled).toBe(true);
expect(output.props['aria-label']).toBe('Save');This inspects a React element object. No button exists in a DOM or native host tree.
Verify a child is passed the right propskeep-child-shallow
renderer.render(<Profile userId="u-42" />);
const output = renderer.getRenderOutput();
const child = output.props.children;
expect(child.type).toBe(Avatar);
expect(child.props.userId).toBe('u-42');Avatar is not executed. This can verify component wiring but cannot catch failures inside Avatar.
Normalize and inspect multiple childreninspect-multiple-children
import React from 'react';
renderer.render(<Toolbar actions={actions} />);
const children = React.Children.toArray(renderer.getRenderOutput().props.children);
expect(children).toHaveLength(2);
expect(children[0].type).toBe(SearchButton);
expect(children[1].type).toBe(MenuButton);The renderer has no find or query API; use the returned element structure or switch to Testing Library for semantic queries.
Rerender the same component with new propsrerender-with-props
const renderer = new ShallowRenderer();
renderer.render(<Status online={false} />);
expect(renderer.getRenderOutput().props.children).toBe('Offline');
renderer.render(<Status online />);
expect(renderer.getRenderOutput().props.children).toBe('Online');Reuse the same renderer and component type to exercise its update path; changing the root component type resets renderer state.
Call an emitted event prop directlyinvoke-callback-prop
const onSave = vi.fn();
renderer.render(<SaveButton onSave={onSave} />);
renderer.getRenderOutput().props.onClick();
expect(onSave).toHaveBeenCalledOnce();Replace vi.fn with jest.fn under Jest. Calling a prop is not real event propagation, default behavior, focus management, or accessibility testing.
Observe a useState updatetest-hook-state
function Counter() {
const [count, setCount] = React.useState(0);
return <button onClick={() => setCount((n) => n + 1)}>{count}</button>;
}
renderer.render(<Counter />);
renderer.getRenderOutput().props.onClick();
expect(renderer.getRenderOutput().props.children).toBe(1);State dispatch is modeled synchronously, but useEffect, useLayoutEffect, and useInsertionEffect never run.
Reach a legacy class instanceaccess-class-instance
class Counter extends React.Component {
state = { count: 0 };
increment = () => this.setState(({ count }) => ({ count: count + 1 }));
render() { return <span>{this.state.count}</span>; }
}
renderer.render(<Counter />);
renderer.getMountedInstance().increment();
expect(renderer.getRenderOutput().props.children).toBe(1);getMountedInstance returns null for function components. Prefer testing rendered behavior when migrating old class tests.
Check a memoized component's comparisontest-memo-rerender
const renderSpy = vi.fn();
const Label = React.memo(({ text }) => {
renderSpy(text);
return <span>{text}</span>;
});
renderer.render(<Label text="same" />);
renderer.render(<Label text="same" />);
expect(renderSpy).toHaveBeenCalledTimes(1);The renderer applies React.memo's custom comparator or a shallow prop comparison, but this still does not exercise a committed host update.
Assert that a component renders nothingtest-conditional-null
function Notice({ visible }) {
return visible ? <aside>Maintenance</aside> : null;
}
renderer.render(<Notice visible={false} />);
expect(renderer.getRenderOutput()).toBeNull();getRenderOutput returns the component's value directly, including null, strings, or React elements.
Snapshot the shallow React elementsnapshot-output
renderer.render(<AccountCard account={account} />);
expect(renderer.getRenderOutput()).toMatchInlineSnapshot();This snapshots one component's returned element tree, not browser HTML or React Native host output. Prefer focused assertions when snapshots churn often.
Exercise class cleanup on unmountunmount-class-component
const cleanup = vi.fn();
class Subscription extends React.Component {
componentWillUnmount() { cleanup(); }
render() { return <Channel id={this.props.id} />; }
}
renderer.render(<Subscription id="news" />);
renderer.unmount();
expect(cleanup).toHaveBeenCalledOnce();componentWillUnmount runs for classes, but componentDidMount and componentDidUpdate are intentionally skipped. Function-component effect cleanups do not run because effects are no-ops.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @testing-library/react | npm | React 18 or 19 web tests should exercise rendered DOM and user-visible behavior |
| @testing-library/react-native | npm | Current React Native tests need queries, events, async updates, and native-aware output |
| @storybook/react-vite | npm | Components need isolated browser rendering plus visual and interaction tests rather than shallow structure checks |