Use MobX reaction() to Sync State with External Side Effects
Learn how MobX reaction() scopes side effects to specific observables, batches updates after transactions, and why you must call the disposer on unmount.
16 Feb 2026, 07:42 UTC

Start with the side effect, not the render
You have an observable user name in MobX. Every time it changes, you want to update document.title or write a preference to localStorage. If you put that logic directly in a React render or in autorun, you will likely run it more often than necessary, or worse, trigger an infinite loop when the external write causes another state change.
The useful takeaway: reaction() lets you declare exactly which observable values should trigger a side effect, runs the effect after the current MobX transaction finishes, and returns a disposer you call when the component unmounts.
How reaction() tracks dependencies
reaction(fn, effect, options?) takes two functions. The first, fn, runs during tracking and returns a value derived from observables. MobX watches only the observables read inside fn. When that returned value changes, effect runs. Unlike autorun, which re-executes whenever any observable it touches changes, reaction ignores unrelated updates.
The effect does not run synchronously in the middle of a state update. MobX schedules it after the active transaction completes, so multiple state changes batch into a single side-effect run. This matters when you update several observables in one action: you do not want three writes to localStorage for one logical change.
Worked example: syncing a name to document.title
Assume a MobX store with an observable displayName. The component below starts a reaction that reads displayName and updates the page title when it changes. It stores the disposer and calls it in React's cleanup function.
import { observable, makeObservable, reaction } from "mobx";
import { useEffect } from "react";
class UserStore {
displayName = "Guest";
constructor() {
makeObservable(this, {
displayName: observable,
});
}
}
const store = new UserStore();
export function UserHeader() {
useEffect(() => {
const dispose = reaction(
() => store.displayName,
(name) => {
document.title = `User: ${name}`;
}
);
return dispose;
}, []);
return {store.displayName};
}
Here fn returns store.displayName. If another observable in the store changes but displayName does not, the effect does not run. When displayName changes, MobX runs the effect after the transaction and updates the title. The returned dispose function unsubscribes the reaction when the component unmounts; without it, the reaction keeps running and can update a detached page or leak memory.
Tuning with options and preventing loops
You can pass an options object as the third argument. delay debounces the effect in milliseconds. fireImmediately runs the effect once when the reaction starts. equals lets you supply a custom comparison for the tracked value.
A common failure mode is writing back into MobX state inside the effect. If fn tracks displayName and the effect updates displayName, the reaction re-runs indefinitely. Keep the effect strictly external—DOM APIs, analytics, network requests—or guard the write with a condition so it only runs when the external value actually differs from the MobX value.
Practical verification
- Open your project terminal and run
grep '"mobx"' package.json. Expect a line like"mobx": "^6.10.0".reactionhas been stable since MobX 4 and behaves the same in MobX 6. - Render the component, change
displayName, and confirmdocument.titleupdates once. - Update a different observable in the same store and confirm the title does not change.
- Unmount the component and confirm no further title updates occur when
displayNamechanges.
If updates still fire too often, check whether fn is reading more observables than intended. If the effect never runs, verify the component is mounted and the reaction disposer was not called prematurely.
Limitation: it is not immediate
Because reaction waits for the transaction boundary, it is the wrong tool when a side effect must happen synchronously before the next line of code. For immediate execution, call the side effect directly inside an action, then use reaction only for deferred synchronization.
Closing
Use reaction when you need to synchronize MobX state with something outside the observable graph. Scope the tracked value narrowly, treat the effect as external-only, and always return the disposer in React cleanup. That pattern keeps side effects predictable and prevents the memory leaks and loops that come from forgetting either half of the contract.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.