# How the MobX State Management System Works in Plane Frontend Applications

> Understand MobX state management in Plane frontend apps. Explore its root store, observable state, actions, and observer HOC for efficient, boilerplate-free UI updates.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: internals
- Published: 2026-06-23

---

**Plane uses a centralized root store pattern with granular domain stores, where observable state is declared via `makeObservable`, mutations are wrapped in `action` decorators, and React components automatically re-render through the `observer` higher-order component, eliminating the need for boilerplate reducers or manual subscription wiring.**

Plane’s frontend is a collection of React applications that rely on MobX for transparent, mutable state management. The architecture centers on a **centralized root store** that aggregates many granular domain-specific stores, each responsible for distinct entities like workspaces, users, issues, and filters. This design allows the UI to react precisely to state changes while maintaining type safety and clear separation between data access and business logic.

## Core Architecture of the Plane MobX State Management System

### Centralized Root Store Pattern

At the heart of Plane’s state layer lies `CoreRootStore` in [`apps/web/core/store/root.store.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/store/root.store.ts). This class instantiates and aggregates every domain store, making them available throughout the component tree via React context.

The root store handles cross-cutting concerns like user sessions and global UI state. When a user signs out, the `resetOnSignOut()` method re-instantiates every store to clear cached data and prevent information leakage between sessions.

```ts
// apps/web/core/store/root.store.ts
import { enableStaticRendering } from "mobx-react";
import { WorkItemFilterStore, UserStore } from "@plane/shared-state";

enableStaticRendering(typeof window === "undefined");

export class CoreRootStore {
  workItemFilters: IWorkItemFilterStore;
  user: IUserStore;

  constructor() {
    this.user = new UserStore(this as unknown as RootStore);
    this.workItemFilters = new WorkItemFilterStore();
  }

  resetOnSignOut() {
    // Re-create every store to discard cached state
    this.user = new UserStore(this as unknown as RootStore);
    this.workItemFilters = new WorkItemFilterStore();
  }
}

```

### Shared State Package vs. App-Specific Stores

Plane separates reusable domain logic from UI-specific concerns using a monorepo structure. The `@plane/shared-state` package contains framework-agnostic stores like `WorkspaceStore` and `UserStore`, while the web application housing `CoreRootStore` adds app-specific stores for issues, projects, and members. This modularization allows consistent state patterns across different Plane frontend applications without duplicating business logic.

## Observable State and Reactivity Patterns

### Declaring Observables with makeObservable

Plane stores use `makeObservable` to explicitly declare which fields should be reactive. Unlike older MobX patterns that rely on decorators, Plane uses the utility-based approach for better TypeScript support and tree-shaking.

In [`packages/shared-state/src/store/workspace.store.ts`](https://github.com/makeplane/plane/blob/main/packages/shared-state/src/store/workspace.store.ts), the `WorkspaceStore` constructor registers each scalar field as an observable reference:

```ts
// packages/shared-state/src/store/workspace.store.ts
import { makeObservable, observable } from "mobx";

export class WorkspaceStore implements IWorkspaceStore {
  id: string;
  name: string;
  createdAt: string;
  updatedAt: string;

  constructor(data: IWorkItemStore) {
    makeObservable(this, {
      id: observable.ref,
      name: observable.ref,
      createdAt: observable.ref,
      updatedAt: observable.ref,
    });
    this.id = data.id;
    this.name = data.name;
    this.createdAt = data.createdAt;
    this.updatedAt = data.updatedAt;
  }
}

```

### Scalar Fields vs. Collection Observables

Plane distinguishes between **scalar observables** using `observable.ref` (ideal for primitives and immutable references) and **deep observables** using `observable` (for Maps, arrays, and objects that require mutation tracking). The `WorkItemFilterStore` in [`packages/shared-state/src/store/work-item-filters/filter.store.ts`](https://github.com/makeplane/plane/blob/main/packages/shared-state/src/store/work-item-filters/filter.store.ts) demonstrates the latter pattern by storing filters in a reactive `Map`.

## Actions and Asynchronous State Mutations

### Synchronous Actions

State mutations in Plane are strictly contained within **actions**. The `makeObservable` configuration maps method names to `action`, ensuring all state changes occur inside MobX transactions for optimal batching and debugging.

### Async Flows with runInAction

For asynchronous operations like API calls, Plane uses `runInAction` to safely mutate observables after async boundaries. The `WorkspaceIssues` store in [`apps/web/core/store/issue/workspace/issue.store.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/store/issue/workspace/issue.store.ts) exemplifies this pattern by updating loader states before and after network requests:

```ts
// apps/web/core/store/issue/workspace/issue.store.ts
import { action, makeObservable, runInAction } from "mobx";

export class WorkspaceIssues extends BaseIssuesStore {
  constructor(_rootStore: IIssueRootStore, issueFilterStore: IWorkspaceIssuesFilter) {
    super(_rootStore, issueFilterStore);
    makeObservable(this, {
      fetchIssues: action,
      fetchNextIssues: action,
    });
    this.workspaceService = new WorkspaceService();
  }

  fetchIssues = async (workspaceSlug, viewId, loadType, options) => {
    try {
      runInAction(() => this.setLoader(loadType));
      const response = await this.workspaceService.getViewIssues(workspaceSlug, params);
      this.onfetchIssues(response, options, workspaceSlug);
      return response;
    } catch (e) {
      runInAction(() => this.setLoader(undefined));
      throw e;
    }
  };
}

```

## Computed Values and Selectors

### Standard Computed Properties

Plane leverages MobX’s `computed` decorator for derived state that automatically updates when dependencies change, though the provided examples primarily showcase `computedFn` from `mobx-utils` for parameterized selectors.

### Memoized Selectors with computedFn

The `WorkItemFilterStore` uses `computedFn` to create cached filter lookups. This prevents expensive recalculations when accessing specific filter configurations by entity type and ID:

```ts
// packages/shared-state/src/store/work-item-filters/filter.store.ts
import { action, makeObservable, observable } from "mobx";
import { computedFn } from "mobx-utils";

export class WorkItemFilterStore implements IWorkItemFilterStore {
  filters: Map<TWorkItemFilterKey, IWorkItemFilterInstance>;

  constructor() {
    this.filters = new Map();
    makeObservable(this, {
      filters: observable,
      getOrCreateFilter: action,
    });
  }

  getFilter = computedFn((entityType, entityId) =>
    this.filters.get(this._getFilterKey(entityType, entityId))
  );
}

```

## React Integration and Component Reactivity

### The Observer Pattern

Components consume stores via a custom `useRootStore()` hook and wrap their export with `observer` from `mobx-react`. This creates a reactive binding where the component re-renders only when the specific observables accessed during its render cycle change:

```tsx
import { observer } from "mobx-react";
import { useRootStore } from "@/hooks/useRootStore";

const WorkspaceTitle = observer(() => {
  const { workspaceRoot } = useRootStore();
  const current = workspaceRoot.currentWorkspace;
  return <h1>{current?.name ?? "Loading…"}</h1>;
});

```

### Static Rendering Support

Plane enables server-side rendering (SSR) safety by calling `enableStaticRendering(typeof window === "undefined")` at the top of [`apps/web/core/store/root.store.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/store/root.store.ts). This prevents MobX from attempting to modify observables during the server render phase, avoiding hydration mismatches.

## Store Lifecycle and Security

### Store Reset on Sign-Out

To prevent data leakage between user sessions, Plane implements a complete store tear-down strategy. The `CoreRootStore.resetOnSignOut()` method described in [`apps/web/core/store/root.store.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/store/root.store.ts) re-instantiates every domain store, guaranteeing that cached API responses, pending promises, and computed values are garbage collected before a new user authenticates.

## Summary

- Plane uses a **centralized root store** (`CoreRootStore`) that aggregates domain-specific stores from the `@plane/shared-state` package and app-specific modules.
- Observables are explicitly declared using `makeObservable` with `observable.ref` for scalars and `observable` for collections.
- State mutations occur strictly within **actions**, with async flows using `runInAction` to safely update state after network requests.
- **Computed selectors** use `computedFn` from `mobx-utils` to provide memoized, efficient data access for complex filter logic.
- React components use the **`observer`** higher-order component to automatically track observable dependencies and re-render only when necessary.
- **Static rendering support** is enabled via `mobx-react`’s `enableStaticRendering` for SSR compatibility.
- Security is enforced by **re-instantiating all stores** on sign-out via `resetOnSignOut()` to clear cached data.

## Frequently Asked Questions

### How does Plane handle async operations in MobX stores?

Plane wraps asynchronous API calls inside class methods decorated as `action`, then uses `runInAction` to mutate observables after `await` statements. This pattern appears in [`apps/web/core/store/issue/workspace/issue.store.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/store/issue/workspace/issue.store.ts) where `fetchIssues` updates loading states before and after network requests, ensuring all observable mutations occur within MobX transactions.

### What is the difference between observable.ref and observable in Plane stores?

Plane uses `observable.ref` for scalar values like strings and numbers where only reassignment needs tracking, while `observable` (deep observation) is used for collections like Maps and arrays. The `WorkspaceStore` in [`packages/shared-state/src/store/workspace.store.ts`](https://github.com/makeplane/plane/blob/main/packages/shared-state/src/store/workspace.store.ts) demonstrates `observable.ref` for workspace metadata, whereas filter stores use deep observables for dynamic collections.

### How do React components access and react to Plane's MobX stores?

Components import `observer` from `mobx-react` and wrap their export to create a reactive boundary. They access stores via the `useRootStore()` hook, then read observable properties directly. The component automatically re-renders when any accessed observable changes, without requiring manual subscription management or selector functions.

### Why does Plane re-instantiate stores on sign-out instead of resetting fields?

Re-instantiating stores via `CoreRootStore.resetOnSignOut()` guarantees complete garbage collection of cached data, pending promises, and computed memoization caches. This approach prevents memory leaks and data leakage between user sessions that could occur if stores simply reset individual fields to initial values.