MobX `when`: A Practical Choice for One-Time Reactive Side Effects
MobX's `when` utility observes a predicate, runs an effect the first time it becomes true, and disposes itself automatically. Learn when to use it, how to avoid leaks, and how to verify the behavior.
05 Aug 2025, 19:55 UTC

The "Just Once" Problem in Reactive UI
In reactive front-ends, you often need to run a side effect exactly once when a condition becomes true: fetch a profile after login, start an animation when a flag flips, or emit an analytics event after a threshold is crossed. The naive approach with React's useEffect forces you to manage dependency arrays and boolean guards, while MobX's general-purpose autorun or reaction will fire every time the underlying observables change. MobX's when utility exists for this narrow but common case: it observes a predicate, runs an effect the first time the predicate returns true, and then disposes itself.
How when Works: A Self-Disposing Reaction
when(predicate, effect, opts?) creates a temporary reaction. The predicate must read at least one observable so MobX can track dependencies. The first time the predicate evaluates to true, MobX runs the effect and immediately disposes the internal reaction. Because disposal is automatic, you do not need to track a "hasRun" flag or manually unsubscribe after success. The function still returns a disposer you can call if the condition never becomes true and you need to clean up early.
Worked Example: Loading Data After Login
Imagine a dashboard that should load user data only after authentication succeeds. The store exposes isAuthenticated as an observable. Inside the component, you call when within a useEffect so the reaction is scoped to the component's lifetime. The examples below assume MobX 6.x and a React function component.
import { when } from 'mobx';
import { useEffect } from 'react';
function UserDashboard({ userStore }) {
useEffect(() => {
const dispose = when(
() => userStore.isAuthenticated,
() => {
loadUserData(userStore.userId);
}
);
return dispose;
}, []);
return <main>{/* render dashboard */}</main>;
}
If isAuthenticated is already true when the component mounts, the effect runs immediately. If it flips to true later, the effect runs then. Either way, it runs once. Returning dispose from the effect ensures that if the component unmounts before login completes, the reaction is torn down rather than left running.
Trade-offs: When when Is the Wrong Tool
when is strictly one-shot. If the predicate can become true, then false, then true again—and you need the effect each time—when is the wrong primitive. Use reaction or autorun instead. More importantly, if the predicate never becomes true, the reaction remains alive until you dispose it or the reactive root is destroyed. In a long-lived component, that is a memory leak. In server-side rendering, failing to dispose can retain reactions across requests, which is especially dangerous because stores may be shared or long-lived on the server.
Verifying Behavior and Disposal
To confirm the behavior, write a Jest test with a mocked store. Use fake timers to control asynchronous predicates.
import { when, observable } from 'mobx';
test('when fires exactly once and disposes', () => {
jest.useFakeTimers();
const store = observable({ isAuthenticated: false });
const effect = jest.fn();
const dispose = when(() => store.isAuthenticated, effect);
store.isAuthenticated = true;
jest.runAllTimers();
expect(effect).toHaveBeenCalledTimes(1);
// Toggling the predicate does not re-trigger the disposed reaction
store.isAuthenticated = false;
store.isAuthenticated = true;
jest.runAllTimers();
expect(effect).toHaveBeenCalledTimes(1);
dispose(); // safe to call even after auto-disposal
});
To verify disposal independently, toggle the predicate back to false and then true again; the original effect should not run a second time because the reaction has already disposed itself. To trigger the effect again, you must call when anew.
Actionable Takeaways
- Use
whenfor one-shot reactions tied to a condition, not for recurring updates. - Always capture the disposer and return it from React effects to avoid leaks if the condition never resolves.
- If your condition can flip multiple times, reach for
reactioninstead. - Verify the behavior with fake timers and a mocked store before shipping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.