Introduction
MobX is a powerful state management library that simplifies reactivity in JavaScript applications. One of its core principles is observability—tracking changes in data to trigger automatic UI updates. However, a common anti-pattern that can break this reactivity is storing references to externally managed objects inside MobX stores.
In this article, we’ll explore:
- Why storing external references is risky.
- Real-world examples of incorrect usage.
- Best practices to avoid these pitfalls.
Why Storing External References is Dangerous
MobX relies on explicit observability—it can only track changes made through MobX’s reactive system (observable
, action
, computed
). If an object is modified outside MobX’s control, components won’t update, leading to stale UI and inconsistent state.
Key Risks:
-
Broken Reactivity – MobX won’t detect changes made outside
@action
. - Unpredictable State – The store no longer fully controls its data.
- Debugging Nightmares – Changes happen silently, making bugs hard to trace.
- Serialization Issues – External objects may contain non-serializable data.
Bad Practice #1: Directly Storing a Mutable External Object
❌ Incorrect Approach
import { makeAutoObservable } from "mobx";
class UserStore {
currentUser: User; // External object (not controlled by MobX)
constructor(user: User) {
makeAutoObservable(this);
this.currentUser = user; // Just stores a reference
}
}
// External mutation (MobX doesn't know about this!)
externalUserService.onUserUpdate((user) => {
currentUser.name = user.name; // Direct mutation (no MobX action)
});
Problem:
- Changes to
currentUser
bypass MobX’s reactivity system. - React components won’t re-render when
user.name
changes.
Bad Practice #2: Storing DOM Elements or Third-Party Library Objects
❌ Incorrect Approach
class UIStore {
chartInstance: Chart; // From a charting library (e.g., Chart.js)
constructor() {
makeAutoObservable(this);
}
setChart(chart: Chart) {
this.chartInstance = chart; // Stores an external reference
}
}
// Later, someone updates the chart directly:
uiStore.chartInstance.data.labels = ["New Data"]; // No MobX reaction!
Problem:
- The chart updates, but MobX doesn’t know about it.
- Any MobX
@computed
orreaction()
depending onchartInstance
will fail to update.
Bad Practice #3: Storing Non-Observable Callbacks or Event Emitters
❌ Incorrect Approach
class NotificationStore {
emitter: EventEmitter; // External event system
constructor(emitter: EventEmitter) {
makeAutoObservable(this);
this.emitter = emitter;
}
}
// External code emits an event:
notificationStore.emitter.emit("message", "Hello!"); // MobX is unaware!
Problem:
- MobX cannot track event emissions.
- If the store relies on
emitter
for state updates, reactivity breaks.
How to Fix This: Best Practices
✅ 1. Convert External Data into Observables
class UserStore {
currentUser: User;
constructor(user: User) {
makeAutoObservable(this, {}, { autoBind: true });
this.currentUser = observable(user); // Makes it reactive
}
}
- Now, even if
currentUser
is modified externally, MobX tracks changes.
✅ 2. Use Actions for Controlled Updates
class UserStore {
currentUser: User;
constructor(user: User) {
makeAutoObservable(this);
this.currentUser = user;
}
// Only allow updates via MobX actions
@action
updateUser(newUser: User) {
this.currentUser = newUser;
}
}
// External service must use the action:
externalUserService.onUserUpdate((user) => {
userStore.updateUser(user); // Proper MobX update
});
✅ 3. Use Reactions to Sync External Changes
import { reaction } from "mobx";
class ChartStore {
chartData: ChartDataType;
constructor() {
makeAutoObservable(this);
// Sync external chart updates with MobX
reaction(
() => externalChart.data,
(newData) => {
this.chartData = newData; // Updates via MobX
}
);
}
}
✅ 4. Store a Copy Instead of a Reference
class SafeStore {
data: SomeExternalType;
constructor(rawData: SomeExternalType) {
makeAutoObservable(this);
this.data = { ...rawData }; // Shallow copy (or deep clone if needed)
}
}
- Ensures the store owns its data and prevents external interference.
Conclusion
Storing external object references in MobX stores is a common trap that breaks reactivity and leads to unpredictable behavior. Instead:
✔ Convert external data into observables.
✔ Control updates via @action
.
✔ Use reaction
to sync external changes.
✔ Avoid direct references—store copies when possible.
By following these best practices, you ensure that MobX’s reactivity works as expected, keeping your UI in sync with your state.
Have you encountered similar issues? Share your experience in the comments! 🚀
Top comments (0)