MobX 6 React rendering boundaries: observer, useObserver, and action enforcement
Use observer and useObserver to isolate React re-renders, and enforce MobX actions for traceable state changes.
20 May 2026, 10:57 UTC

Issue and Takeaway
A MobX store update can trigger more React re-renders than necessary when the rendering boundary is too large. The useful takeaway is to place observer on the smallest presentational components, isolate expensive subtrees with useObserver, and enable enforceActions: 'always' so mutations stay inside actions and are traceable.
Smallest Suitable Design
Keep mutable state in stores. Expose observables as props or plain values, and wrap only the leaf components that read them with observer. This creates a fine-grained subscription boundary: a component re-renders only when an observable it read during its own render changes.
- Store: holds observables and actions.
- Observable: a value MobX tracks when read during render.
- Observer component: subscribes to observables read during render and re-renders only on those changes.
Observer and useObserver
observer from mobx-react-lite wraps a whole component. useObserver wraps a render callback inside a component, so a large component can keep its shell static while only the observed subtree updates.
MobX tracks reads lazily during render. Values read outside render, inside effects, or after an await gap are not automatically subscribed. Place the boundary where the read actually happens.
Action-Based Mutation Boundary
MobX 6 permits direct observable mutation outside an action, but that defeats tracing and can produce silent, hard-to-reproduce state changes. Setting enforceActions: 'always' forces mutations through actions or runInAction.
import { configure } from 'mobx';
configure({ enforceActions: 'always' });
Run this once during application setup, before stores are created. It is process-scoped and affects the current module graph.
Concrete Example
The example below shows a store, a leaf component wrapped with observer, and a large component that isolates an expensive subtree with useObserver.
// store.js
import { makeObservable, observable, action } from 'mobx';
export class CounterStore {
count = 0;
constructor() {
makeObservable(this, {
count: observable,
increment: action,
});
}
increment() {
this.count += 1;
}
}
// CounterItem.jsx
import { observer } from 'mobx-react-lite';
export const CounterItem = observer(({ store }) => {
return Count: {store.count};
});
// LargeComponent.jsx
import { useObserver } from 'mobx-react-lite';
export const LargeComponent = ({ store }) => {
return (
<>
Static shell that does not read observables
{useObserver(() => {
return Heavy UI for {store.count};
})}
);
};
Incrementing the counter re-renders CounterItem and the useObserver subtree, but not the surrounding shell of LargeComponent. Updating an unrelated observable in the same store does not re-render either component unless they read that observable.
Operational Checks for Re-render Failures
- Render a store value in an
observercomponent. - Update an unrelated observable and confirm the component does not re-render.
- Enable
enforceActions: 'always'in a test file, mutate an observable directly outside an action, and confirm an error is thrown. - Wrap a render callback with
useObserverinside a large component, update the store, and confirm only the hooked subtree re-renders.
Where to run: in a local test environment or browser dev tools. Required permissions: none beyond normal app execution. Expected check: component render count stays flat for unrelated updates. Risk: enabling enforceActions globally can break legacy code that mutates observables outside actions.
Limitations and Failure Modes
- Wrapping container components with
observercan cause extra re-renders if they read many observables or pass new object/array references as props. observerdoes not perform deep prop comparison, so new references each render can trigger child re-renders.- MobX 6 uses
makeObservableby default; omitting the constructor call leaves fields unobserved. - Mutating observables in event handlers without an action or
runInActioncreates silent, untracked state changes.
When the Design Must Change
The smallest design fits typical client-side React apps. Conditions that require a different approach include:
- Server-side rendering, where MobX state must be hydrated correctly.
- Integration with non-React UIs that do not understand observer subscriptions.
- Deterministic action replay, such as time-travel debugging or recorded tests.
In those scenarios, adjust the rendering boundary or restructure state handling before adopting the pattern above.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.