Managing Side Effects with MobX autorun
Stop writing manual subscriptions. Learn how MobX autorun uses dynamic dependency tracking and microtask scheduling to keep your side effects in sync with your state.
25 Oct 2025, 19:44 UTC

The problem: Manual subscription fatigue
In complex state management, developers often find themselves writing repetitive boilerplate to sync the UI or trigger API calls whenever a specific piece of state changes. Manually adding and removing event listeners or observers is error‑prone; missing one subscription leads to a stale interface, while forgetting to unsubscribe causes memory leaks.
The thesis
MobX autorun solves this by automatically tracking which observables are accessed during execution and re‑running the effect only when those specific values change. By leveraging microtask scheduling, it ensures that updates are batched and consistent, removing the need for manual dependency arrays or subscription management.
How dependency tracking works
When you wrap a function in autorun, MobX executes that function immediately. During this first run, MobX records every observable property that is read. This creates a dynamic dependency map. If any of those recorded observables are modified later, the function is scheduled to run again.
Because MobX batches synchronous modifications made inside an action, the autorun will not fire mid‑transaction. It waits until the action completes and then executes as a microtask, preventing the application from rendering inconsistent, partial state updates.
Fine‑tuning the reaction
You can modify the default behavior using an options object:
- delay: Debounces the reaction, which is useful for expensive operations like API calls during rapid typing.
- equals: A custom comparison function to prevent the reaction from firing if the new value is logically equivalent to the old one.
- fireImmediately: Controls whether the effect runs once immediately upon creation.
Worked Example: Synchronizing a User Profile
This example demonstrates a store that tracks a user's status and an autorun that handles the side effect of updating a document title. This can be run in a Node.js environment with npm install mobx.
import { makeAutoObservable, autorun, action } from 'mobx';
class UserStore {
name = 'Guest';
status = 'Offline';
constructor() {
makeAutoObservable(this);
}
// Action to update user info
updateUser(newName, newStatus) {
this.name = newName;
this.status = newStatus;
}
}
const userStore = new UserStore();
// The autorun tracks only the observables accessed inside the function
const disposer = autorun(() => {
console.log(`[Side Effect] Updating title to: ${userStore.name} is ${userStore.status}`);
});
// Triggering a change inside an action
action(() => {
userStore.updateUser('Alice', 'Online');
})();
// Clean up the reaction to prevent memory leaks
disposer();
Verification: To verify this behavior, run the script and observe the console. You should see the log appear exactly once after the updateUser action completes, despite two separate properties being changed. If you remove the action wrapper and change properties individually, the timing of the microtask may vary depending on the environment's event loop.
The "Conditional Read" Pitfall
A common engineering mistake is accessing observables inside a conditional block. MobX only tracks dependencies that are actually read during the execution of the function.
Consider this scenario:
autorun(() => {
if (userStore.status === 'Online') {
console.log('User is active: ' + userStore.name);
}
});
If the status is 'Offline', MobX never reads userStore.name. Consequently, if the name changes while the user is offline, the autorun will not trigger. If the status then switches to 'Online', the reaction will finally fire, but it will have missed all intermediate name changes. To avoid this, ensure all required observables are dereferenced unconditionally if they must always trigger the reaction.
Performance Limitations
Since autorun executes as a microtask, placing heavy synchronous computation (like large array sorting or complex data transformation) inside the reaction can block the microtask queue. This delays other reactions and can cause the UI to stutter. For heavy lifting, offload the work to a Web Worker or use setTimeout to move the execution to the next event loop tick.
Actionable Summary
Use autorun for side effects that must stay in sync with your state without manual wiring. To implement it safely: first, define your store with makeAutoObservable; second, wrap your side effect in autorun; third, ensure all dependencies are read unconditionally; and finally, store the returned disposer function to clean up the reaction when the associated component or service is destroyed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.