DEV Community

Maxim Logunov
Maxim Logunov

Posted on

The Pitfalls of Storing External Object References in MobX Stores

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:

  1. Broken Reactivity – MobX won’t detect changes made outside @action.
  2. Unpredictable State – The store no longer fully controls its data.
  3. Debugging Nightmares – Changes happen silently, making bugs hard to trace.
  4. 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)
});
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

Problem:

  • The chart updates, but MobX doesn’t know about it.
  • Any MobX @computed or reaction() depending on chartInstance 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!
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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
});
Enter fullscreen mode Exit fullscreen mode

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
      }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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)