react-freeze
react-freeze exports one React component that prevents a mounted subtree from rendering while a boolean is true. It uses a Suspense boundary and a never-resolving thenable, swaps the visible subtree for an optional placeholder, and keeps the same DOM or React Native views and component state for later. Its intended use is fully hidden navigation screens whose state and scroll position should survive while unnecessary reconciliation stops.
react-freeze is a precise performance tool for already-hidden mounted screens, not a general memoization primitive. Measure first, keep lifecycle work outside the frozen boundary, and prefer navigation-native support when it already exists.
Use it if
- A measured performance problem comes from hidden but mounted React or React Native screens re-rendering
- You must preserve component state, native view instances, scroll position, input state, and loaded images
- A subtree is guaranteed to be non-interactive and visually covered whenever it is frozen
- You use a supported React 17 or newer runtime and can test Suspense behavior across navigation transitions
- The subtree remains visible or interactive: the README says frozen children are replaced by the placeholder or null and should normally be hidden from the user
- Surrounding layout depends on the frozen subtree's dimensions; the README warns replacement can move the rest of the interface unless the placeholder preserves space
- You expect background work to pause: state updates and Redux selectors still run while rendering is frozen, and mounted view state is retained
- You need reliable profiling with React Native Debugger: the README documents a profiler error when frozen components are present
- You are adopting current React Navigation solely for this feature: the integration guide still targets React Navigation 5 or 6 and react-native-screens 3.9.0, while direct use adds a Suspense trick your navigation layer may already manage
Setup reality
Install `react-freeze`; version 1.0.4 declares React 17 or newer as its only peer and Node 10 or newer for tooling, with CommonJS, modern module, and bundled TypeScript declaration entries. Direct use is one `<Freeze freeze={boolean}>` boundary. The simplicity hides behavioral work. When frozen, children do not render and the boundary shows `placeholder`, defaulting to null, but the existing DOM or native view instances and React state remain for defrost. Only freeze content that is completely hidden. If sibling layout depends on its box, supply a correctly sized placeholder or use absolute positioning. State changes still happen and Redux selectors still execute; the latest state appears in one render after defrost. Freezing does not unmount the subtree, so it is not a lifecycle signal for stopping sockets, timers, media, polling, or native work. Stop that work separately before freezing. Render-time side effects may be skipped across updates, which the README lists as a way behavior can change. For React Native navigation, the documented path requires React Native 0.64 or later, React Navigation 5 or 6, react-native-screens 3.9.0, an iOS pod install, and `enableFreeze(true)`; it intentionally leaves the top two stack screens unfrozen for swipe-back transitions. That recipe is dated relative to current navigation releases, so verify the navigation package's own current guidance before adding react-freeze directly. Profile on target devices first, because an extra Suspense boundary is only justified by measured hidden-screen render cost.
Patterns
Freeze a subtree only while it is hiddenfreeze-hidden-subtree
import { Freeze } from 'react-freeze';
function HiddenPanel({ hidden, children }) {
return (
<Freeze freeze={hidden}>
<section aria-hidden={hidden}>{children}</section>
</Freeze>
);
}When frozen, the section is replaced by null, so this is appropriate only when the user should not see or interact with it.
Keep layout space with a placeholderpreserve-layout-placeholder
<Freeze
freeze={!visible}
placeholder={<div style={{ width: 320, height: 240 }} aria-hidden="true" />}
>
<ExpensiveChart />
</Freeze>The README warns that the default null fallback can reflow siblings; the placeholder must match the layout you need to preserve.
Freeze mounted inactive tab panelsfreeze-inactive-tabs
function Tabs({ activeId, tabs }) {
return tabs.map((tab) => (
<Freeze key={tab.id} freeze={tab.id !== activeId}>
<div role="tabpanel" aria-label={tab.label}>
<tab.Component />
</div>
</Freeze>
));
}This retains every tab's state and native nodes, which saves rerenders but also retains their memory.
Freeze only deeply covered stack screensfreeze-covered-stack-screens
function StackScreen({ depthFromTop, children }) {
const covered = depthFromTop > 1;
return <Freeze freeze={covered}>{children}</Freeze>;
}The documented navigation integration leaves the top and second screen active so swipe-back can reveal the previous screen correctly.
Enable react-native-screens integrationenable-native-navigation-freeze
import { enableFreeze } from 'react-native-screens';
enableFreeze(true);The react-freeze README documents this for React Navigation 5 or 6 and react-native-screens 3.9.0; check current screens documentation before copying it into a newer stack.
Retain a hidden form without unmountingpreserve-form-state
function DraftStep({ active }) {
return (
<Freeze freeze={!active}>
<div style={{ display: active ? 'block' : 'none' }}>
<DraftForm />
</div>
</Freeze>
);
}Input and component state survive defrost, but so does memory; do not keep an unlimited number of completed forms mounted.
Stop polling separately from render freezingstop-work-before-freeze
function ScreenSlot({ active }) {
useEffect(() => {
if (!active) return;
const timer = setInterval(refresh, 30000);
return () => clearInterval(timer);
}, [active]);
return <Freeze freeze={!active}><Screen /></Freeze>;
}Keep this effect above the boundary. Frozen children do not receive the render that would let their own effect observe `active=false`.
Wait for a navigation transition before freezingfreeze-after-transition
function RouteSlot({ focused, transitionRunning, children }) {
const shouldFreeze = !focused && !transitionRunning;
return <Freeze freeze={shouldFreeze}>{children}</Freeze>;
}Freezing a screen that is still visible during an animation replaces it with the fallback and can create a flash or broken gesture.
Avoid sibling reflow for stacked screensfreeze-absolutely-positioned-screen
<div style={{ position: 'absolute', inset: 0, visibility: active ? 'visible' : 'hidden' }}>
<Freeze freeze={!active}>
<Screen />
</Freeze>
</div>Absolute positioning matches the README's recommended case where removing frozen content does not reposition unfrozen siblings.
Test that updates appear after defrosttest-deferred-render
const { rerender, getByText, queryByText } = render(
<Freeze freeze={false}><Counter value={1} /></Freeze>,
);
expect(getByText('1')).toBeTruthy();
rerender(<Freeze freeze={true}><Counter value={2} /></Freeze>);
expect(queryByText('2')).toBeNull();
rerender(<Freeze freeze={false}><Counter value={2} /></Freeze>);
expect(getByText('2')).toBeTruthy();State and props can change while frozen, but the subtree renders the latest values only after the boundary defrosts.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| react-native-screens | npm | React Native navigation should manage inactive native screens and its built-in freeze integration |
| react-activation | npm | A web app needs keep-alive activation and deactivation semantics around cached routes |
| react-keep-alive | npm | You need a component cache that preserves web route state across removal from the visible tree |