# How @plane/shared-state Manages MobX Stores: A Deep Dive into Plane's State Architecture

> Discover how @plane/shared-state manages MobX stores with plain TypeScript classes, makeObservable, actions, and computed values for robust state architecture in Plane.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: deep-dive
- Published: 2026-08-23

---

**The `@plane/shared-state` package implements MobX stores as plain TypeScript classes using `makeObservable` to define observables, `action` for state mutations, and `computed`/`computedFn` for derived data, without enforcing global singletons.**

The `@plane/shared-state` package sits at the heart of Plane's client-side state management, providing a collection of reactive stores built on MobX. These stores handle everything from workspace metadata to complex work-item filtering logic, following a consistent pattern that balances performance with developer ergonomics. Understanding how this package manages MobX stores reveals the architectural decisions behind Plane's reactive UI layer.

## Core Store Architecture

Each store in `@plane/shared-state` follows the same fundamental structure: a TypeScript class whose properties become MobX observables through explicit `makeObservable` calls in the constructor.

### WorkspaceStore: Simple Observable References

The `WorkspaceStore` demonstrates the minimal pattern for basic data models. Located at [`src/store/workspace.store.ts`](https://github.com/makeplane/plane/blob/main/src/store/workspace.store.ts), it holds workspace metadata as observable references to avoid unnecessary deep conversion:

```typescript
// From src/store/workspace.store.ts
makeObservable(this, {
  id: observable.ref,
  name: observable.ref,
  logo_url: observable.ref,
  created_at: observable.ref,
  updated_at: observable.ref,
})

```

Primitive values use `observable.ref` rather than plain `observable`, which prevents MobX from recursively making properties observable—an optimization since these fields are simple strings or timestamps.

### UserStore: Maps and Complex State

The `UserStore` ([`src/store/user.store.ts`](https://github.com/makeplane/plane/blob/main/src/store/user.store.ts)) illustrates handling more complex structures. It tracks the current user alongside a Map of workspaces, with different observable strategies for each:

```typescript
makeObservable(this, {
  user: observable.ref,
  workspaces: observable,  // Map gets full observable treatment
  isLoading: observable,
  errors: observable,
})

```

The `workspaces` Map remains observable for reactivity, allowing UI components to respond when workspaces are added or removed via standard Map operations.

### WorkItemFilterStore: Action-Heavy State Management

For interactive state requiring frequent updates, the `WorkItemFilterStore` ([`src/store/work-item-filters/filter.store.ts`](https://github.com/makeplane/plane/blob/main/src/store/work-item-filters/filter.store.ts)) demonstrates the action pattern. Its `filters` Map holds per-entity filter instances, with methods wrapped as MobX actions:

```typescript
// Key methods declared as actions in makeObservable
getOrCreateFilter: action,
resetExpression: action,
updateFilterValueFromSidebar: action,
deleteFilter: action,

```

Actions batch notifications, preventing excessive re-renders during complex filter operations.

### FilterConfig: Computed Properties and memoizedFn

The rich-filter system in [`src/store/rich-filters/config.ts`](https://github.com/makeplane/plane/blob/main/src/store/rich-filters/config.ts) showcases derived state through `computed` values and `computedFn` from *mobx-utils*:

```typescript
makeObservable(this, {
  // Observables
  selectedValues: observable,
  selectedOperations: observable,
  
  // Computed values
  allEnabledSupportedOperators: computed,
  firstOperator: computed,
  
  // Actions
  mutate: action,
})

```

Methods like `getOperatorConfig` and `getAllDisplayOperatorOptionsByValue` use `computedFn` for efficient memoization—these expensive lookups recompute only when their observable inputs change.

## Reactive Patterns in Practice

### Observable Strategy by Data Type

| Data Type | MobX Decorator | Rationale |
|-----------|---------------|-----------|
| Primitives (string, boolean, Date) | `observable.ref` | Avoid deep conversion overhead |
| Collections (Map, Set, Array) | `observable` | Enable reactive iteration and structural changes |
| Class instances | `observable.ref` or nested `makeObservable` | Depends on reactivity needs |

### Action Boundaries

State mutations flow exclusively through `action`-decorated methods. This constraint enables MobX's transaction batching: multiple property changes within a single action notify observers only once. The `updateFilterValueFromSidebar` method in `WorkItemFilterStore` may touch several observables, but components re-render once, not per mutation.

### Computed Optimization

Heavy derivation logic uses `computedFn` rather than standard `computed` when the computation depends on arguments. Standard `computed` properties work for derivations from fixed observables; `computedFn` extends this to parameterized functions with proper memoization keyed by arguments.

## Store Instantiation and Lifecycle

Unlike many MobX patterns, `@plane/shared-state` deliberately avoids global singletons. Consumers instantiate stores as needed:

```typescript
// Component-level or higher store instantiation
import { WorkspaceStore, UserStore, WorkItemFilterStore } from "@plane/shared-state";

// Simple store creation
const workspace = new WorkspaceStore({
  id: "w-123",
  name: "Engineering",
  logo_url: "/assets/engineering.svg",
  created_at: new Date().toISOString(),
  updated_at: new Date().toISOString(),
});

```

This instantiation pattern supports testing and server-side rendering scenarios where global state causes conflicts. The `WorkItemFilterStore` internally manages its own child objects—`FilterInstance` creations happen through `getOrCreateFilter`, which handles the lifecycle of per-entity filter state.

## Practical Usage Examples

### Reactive Workspace Display

```tsx
import { observer } from "mobx-react-lite";
import { WorkspaceStore } from "@plane/shared-state";

const workspace = new WorkspaceStore({ /* ... */ });

const WorkspaceHeader = observer(() => (
  <header>
    <h1>{workspace.name}</h1>
    <span>Last updated: {new Date(workspace.updated_at).toLocaleDateString()}</span>
  </header>
));

```

The `observer` HOC connects React's render cycle to MobX's dependency tracking—no manual subscription management required.

### User and Workspace Coordination

```typescript
const userStore = new UserStore();

// Set authenticated user
userStore.user = { id: "u-42", email: "dev@plane.so" };

// Associate workspace with user
userStore.workspaces.set(workspace.id, workspace);

// Reactivity extends to Map operations
console.log(userStore.workspaces.size); // Triggers updates on add/delete

```

### Complex Filter Management

```typescript
const filterStore = new WorkItemFilterStore();

// Initialize filter for a specific project view
const projectFilter = filterStore.getOrCreateFilter({
  entityId: "proj-789",
  entityType: "PROJECT",
  showOnMount: true,
});

// Sidebar interaction adds filter condition
filterStore.updateFilterValueFromSidebar("PROJECT", "proj-789", {
  property: "priority",
  operator: "is_any_of",
  value: ["urgent", "high"],
});

// Reset to initial state
filterStore.resetExpression("PROJECT", "proj-789");

```

The `updateFilterValueFromSidebar` method encapsulates the complexity of converting UI interactions into the internal filter expression format, handling AND/OR logic and operator validation transparently.

## Package Export Structure

All stores surface through [`src/store/index.ts`](https://github.com/makeplane/plane/blob/main/src/store/index.ts) with organized namespaces:

```typescript
// From src/store/index.ts
export * from "./user.store";
export * from "./workspace.store";
export * from "./instance.store";
export * from "./work-item-filters/filter.store";
export * from "./rich-filters";

```

This single entry point enables clean imports while maintaining internal modularity.

## Summary

- `@plane/shared-state` implements **MobX stores as explicit TypeScript classes** with `makeObservable` declarations rather than decorators
- **`observable.ref`** optimizes primitive storage while **`observable`** enables reactive Maps and collections
- **Actions batch mutations** to minimize re-renders, with `computed` and `computedFn` providing efficient derived data
- **No singleton enforcement** allows flexible instantiation patterns for testing and SSR compatibility
- **File locations**: [`src/store/workspace.store.ts`](https://github.com/makeplane/plane/blob/main/src/store/workspace.store.ts), [`src/store/user.store.ts`](https://github.com/makeplane/plane/blob/main/src/store/user.store.ts), [`src/store/work-item-filters/filter.store.ts`](https://github.com/makeplane/plane/blob/main/src/store/work-item-filters/filter.store.ts), [`src/store/rich-filters/config.ts`](https://github.com/makeplane/plane/blob/main/src/store/rich-filters/config.ts)

## Frequently Asked Questions

### What MobX version does @plane/shared-state use?

The package uses MobX v6 with the explicit `makeObservable` API rather than legacy decorators. This choice aligns with modern MobX recommendations and avoids Babel/TypeScript decorator configuration complexity. The *mobx-utils* companion package provides `computedFn` for memoized function computations.

### Why use observable.ref instead of observable for primitives?

`observable.ref` treats values as opaque references, skipping MobX's automatic deep conversion. For `WorkspaceStore` fields like `id` and `created_at`, this prevents the overhead of making every string character or Date method reactive. The optimization matters for stores instantiated frequently across large applications.

### How does WorkItemFilterStore handle multiple entity types?

The store maintains an internal `filters` Map keyed by composite identifiers combining `entityType` and `entityId`. The `getOrCreateFilter` method implements a factory pattern, returning existing instances or creating new `FilterInstance` objects with their own MobX observables. This design isolates filter state per view while sharing the management infrastructure.

### Can stores be used outside React components?

Yes—the MobX stores have no React dependency. The observable-action-computed pattern works with any JavaScript consumer. Plane likely uses these stores in non-React contexts such as background workers, CLI tools, or server-side data preparation. React integration happens through `mobx-react-lite`'s `observer`, not through the store implementation itself.