The Developer's Quarterly · Testing ToolsVol. III · March 2026

jest.isolateModules:Why It Exists and When It Matters

jest.isolateModules gives each callback its own module registry, which is useful when a test needs a clean import graph without resetting the whole suite.

If you write React tests long enough, you eventually hit a strange failure: one test changes a module once, and another test sees that same module in a different state. The code looks fine, but the result is not stable. That is the problem jest.isolateModules is built to solve.

It gives Jest a fresh module registry inside a callback, so anything you load there gets its own copy of the imports. That is especially useful for stateful modules, load-time configuration, and React app code that reads environment values at import time.

What jest.isolateModules actually does

Jest normally caches modules after the first import. That is good for speed, but it also means module-level state can leak from one test to another.

jest.isolateModules(fn) creates a sandbox registry for everything loaded inside fn. Modules required inside the callback do not share the same cache as modules loaded outside it. Once the callback finishes, the sandbox copy is discarded.

1 Fresh registry inside the callback

Modules loaded there get their own private cache and do not reuse the file-level instance.

2 Useful for stateful imports

Singletons, feature flags, and module-level counters are the common pain points.

3 Narrower than a full reset

You isolate just the import graph you care about instead of clearing the whole suite.

The bug it solves

Here is the classic failure mode: a module keeps state in a top-level variable.

Stateful module
// counter.js let count = 0; function increment() { count += 1; return count; } function getCount() { return count; } module.exports = { increment, getCount, };

If two tests import that module directly, they share the same cached instance for the duration of the file.

Leaky test pattern
const counter = require('./counter'); test('first test increments from zero', () => { expect(counter.increment()).toBe(1); }); test('second test expects a fresh module', () => { expect(counter.getCount()).toBe(0); });

The second test is not guaranteed to see a clean module. The count can already be 1 because the module was loaded once and then reused.

The fix

Load the module inside jest.isolateModules so each test gets its own copy.

Isolated load
test('each isolated load starts clean', () => { let counter; jest.isolateModules(() => { counter = require('./counter'); }); expect(counter.getCount()).toBe(0); expect(counter.increment()).toBe(1); });

That pattern matters when a module does real work at import time. A singleton store, a feature-flag helper, a configuration object, or a cache layer can all behave differently if the cached copy leaks across tests.

Why React teams care

React apps often import settings or utilities once and then let components depend on them. If those modules read from process.env, build a singleton, or capture a value during import, you may need a clean copy for each scenario you test.

Feature flag module
// featureFlags.js const betaEnabled = process.env.NEXT_PUBLIC_BETA === '1'; module.exports = { betaEnabled, };
App module
// App.js const { betaEnabled } = require('./featureFlags'); function App() { return betaEnabled ? 'beta dashboard' : 'classic dashboard'; } module.exports = { App, };

In tests, you can set the environment, isolate the module load, and verify the branch you expect.

Environment-specific test
test('loads the beta branch when the flag is enabled', () => { const previous = process.env.NEXT_PUBLIC_BETA; process.env.NEXT_PUBLIC_BETA = '1'; let App; jest.isolateModules(() => { App = require('./App').App; }); expect(App()).toBe('beta dashboard'); process.env.NEXT_PUBLIC_BETA = previous; });

That is the practical React use case: the component itself may be pure, but the module it depends on is not.

resetModules vs isolateModules

API What it changes When to use it
jest.resetModules() Clears the shared module cache for the file When you want a full reset between tests
jest.isolateModules(fn) Creates a sandbox registry only inside the callback When one test needs a clean copy without affecting the rest
jest.isolateModulesAsync(fn) Does the same thing for async callbacks When the module under test is imported with dynamic import()

resetModules is broader. isolateModules is narrower and often easier to reason about when only one import path needs isolation.

Async modules need the async version

If your code uses dynamic imports or you want to test an ESM module, use jest.isolateModulesAsync.

Async isolation
test('loads an ESM module in isolation', async () => { let config; await jest.isolateModulesAsync(async () => { config = await import('./config.js'); }); expect(config.featureName).toBe('beta'); });

The rule is simple: if you need await, use the async version.

When it actually matters

1 Module-level state

Use it when a module stores data in top-level variables or singletons.

2 Load-time branching

Use it when a module reads environment values or chooses a path during import.

3 Per-test setup differences

Use it when one test needs a mocked dependency or configuration that should not affect the next one.

Practical rules of thumb

Load inside the callback If you require the module before the callback, the isolated registry cannot help.
Use the smallest scope possible Only isolate the import graph that needs the fresh copy.
Prefer the async variant for ESM That keeps dynamic imports and modern module syntax straightforward.
Do not expect it to reset everything It isolates module loading, not timers, DOM state, or global variables you changed yourself.

Bottom line

jest.isolateModules exists for a narrow but real problem: module cache leakage. If your test suite touches stateful modules, import-time branching, or React app code that depends on module initialization, it gives you a clean way to load just that part of the graph in isolation.

In most tests, you will not need it. When you do need it, though, it is usually the right fix because it targets the problem directly instead of resetting more of the world than necessary.