Batching State Changes in MobX with Transactions: A Practical Guide
Learn how to use MobX transactions to batch multiple observable updates in a React app. A step‑by‑step example shows a counter that increments ten times with only one re‑render, plus trade‑offs and verification tips.
04 Sept 2026, 23:49 UTC

Why Batching Matters in React + MobX
When a React component reads from a MobX observable, every change to that observable triggers a re‑render. If a component updates a large object tree or performs a loop that mutates many observables, the UI can flicker or lag because it reacts after each mutation. MobX transactions let you group those changes together so that reactions fire only once, after the group finishes.
What is a MobX Transaction?
A transaction is a synchronous wrapper provided by mobx:
import { transaction } from 'mobx';
transaction(() => {
// multiple observable writes
});
Inside the callback, all observable mutations are recorded. When the callback returns, MobX runs any pending reactions, computed values, or observers exactly once. This is equivalent to runInAction for a single mutation, but it batches multiple changes.
When to Use Transactions
- Importing a large JSON payload into an observable store.
- Resetting several form fields to default values.
- Updating a nested object or array in a loop.
- Any scenario where you want to avoid intermediate UI states.
Concrete Worked Example
Below is a minimal React component that demonstrates batching with a button click. The component displays a counter that is an observable in a MobX store. Clicking the button increments the counter ten times inside a transaction, causing the component to re‑render only once.
import React from 'react';
import { makeAutoObservable, transaction } from 'mobx';
import { observer } from 'mobx-react-lite';
class CounterStore {
count = 0;
constructor() { makeAutoObservable(this); }
increment() { this.count += 1; }
}
const store = new CounterStore();
const Counter = observer(() => {
const handleClick = () => {
transaction(() => {
for (let i = 0; i < 10; i++) {
store.increment();
}
});
};
return (
<div>
<h3>Count: {store.count}</h3>
<button onClick={handleClick}>Increment 10 Times</button>
</div>
);
});
export default Counter;
Running this component in a browser will show the counter jump from 0 to 10 instantly. If you replace the transaction wrapper with a simple loop, the component would re‑render ten times, potentially causing noticeable lag in larger applications.
How to Verify the Batching Effect
- Install
mobx-react-liteandmobx-devtools-browser.- Run
npm install mobx mobx-react-lite mobx-devtools-browser.
- Run
- Wrap your React app with
MobXProviderif using stores. - Open the browser console and enable MobX DevTools.
- You should see a single reaction trace for the
Countercomponent after the button click, confirming that only one update occurred.
- You should see a single reaction trace for the
- Optionally, use React Profiler to confirm that the component updates only once.
Trade‑offs and Limitations
- Only synchronous: Transactions cannot span asynchronous operations. If you need to batch updates that happen over time, consider using
runInActioninside each async callback or a custom batching strategy. - Developer discipline: Forgetting to wrap a bulk update in a transaction can negate the performance benefit. A code review checklist can help enforce the pattern.
- Side effects: If you perform side effects (e.g., network requests) inside a transaction, they will execute immediately, not after the batch completes. Keep side effects outside the transaction block.
Actionable Takeaway
For any component or store method that mutates many observables at once, wrap the changes in transaction(() => { … }). Verify the effect with MobX DevTools or React Profiler. Remember that transactions are strictly synchronous, so they’re best suited for bulk imports, resets, or loops that run in a single call stack.
Further Reading
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.