How Plane Differentiates Between Server State and Client State: SWR vs MobX Architecture

Plane uses SWR to manage server state (remote API data with caching and revalidation) and MobX to handle client state (local UI observables and transient data), maintaining strict architectural boundaries between the two.

In the makeplane/plane codebase, state management is split into two distinct layers based on data origin and lifecycle requirements. Understanding how Plane differentiates between server state and client state is essential for contributing to the frontend architecture or building custom extensions. This separation leverages SWR for HTTP‑centric remote data and MobX for reactive local application state.

Architectural Separation: Server State vs Client State

Plane draws a clear line between where data lives and how it flows:

Aspect Server State (SWR) Client State (MobX)
Library SWR – React hooks for data fetching, caching, and revalidation MobX – Observable state containers with automatic dependency tracking
Data Source Backend APIs (e.g., workspace labels, cycles, estimates) UI‑centric values (e.g., selected workspace, loading flags, panel visibility)
Location Hook files under apps/web/core/hooks/ Store files under packages/shared-state/src/store/
Lifecycle Fetched on demand, cached globally by SWR, revalidated on focus or interval Instantiated at app startup, persists for the session, updates propagate automatically
Mutability Immutable cache entries; refreshed via API calls Mutable observables updated through actions

Server State Management with SWR

Plane uses SWR for any data that originates from the backend and requires caching, stale‑while‑revalidate strategies, or automatic background updates.

Implementation in Hooks

SWR logic is encapsulated in custom hooks under apps/web/core/hooks/. For example, useWorkspaceIssueProperties fetches workspace metadata using SWR’s useSWR hook with specific fetch keys:

// apps/web/core/hooks/use-workspace-issue-properties.ts
import useSWR from "swr";
import { WORKSPACE_LABELS, WORKSPACE_CYCLES } from "@/constants/fetch-keys";

export const useWorkspaceIssueProperties = (workspaceSlug) => {
  // fetch workspace labels
  useSWR(
    workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
    workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null,
    { revalidateIfStale: false, revalidateOnFocus: false }
  );

  // fetch workspace cycles
  useSWR(
    workspaceSlug ? WORKSPACE_CYCLES(workspaceSlug.toString()) : null,
    workspaceSlug ? () => fetchWorkspaceCycles(workspaceSlug.toString()) : null,
    { revalidateIfStale: false, revalidateOnFocus: false }
  );
};

The hook returns SWR’s standard API (data, error, mutate, isValidating), keeping components agnostic to the underlying HTTP implementation.

Lifecycle and Caching

SWR stores data in a global cache keyed by unique strings (e.g., WORKSPACE_LABELS(workspaceSlug)). This guarantees that multiple components subscribing to the same key receive identical data without duplicate network requests. According to the Plane source code, fetch keys are centralized in packages/constants/src/swr.ts to ensure consistency across the application.

Client State Management with MobX

For transient UI state that never requires server round‑trips, Plane implements MobX stores in packages/shared-state/src/store/.

Store Structure and Observables

MobX stores are plain classes decorated with makeObservable, converting properties into observable refs and methods into actions:

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

export class WorkspaceStore {
  id = "";
  name = "";
  createdAt = "";
  updatedAt = "";
  // …other props

  constructor() {
    makeObservable(this, {
      id: observable.ref,
      name: observable.ref,
      createdAt: observable.ref,
      updatedAt: observable.ref,
      setWorkspace: action,
    });
  }

  setWorkspace(data: Partial<WorkspaceStore>) {
    runInAction(() => {
      Object.assign(this, data);
    });
  }
}

Components consume these stores through the observer HOC from mobx-react-lite, ensuring automatic re‑renders when observed properties change.

Reactive Updates

Because MobX tracks dependencies automatically, UI toggles like workspaceStore.isPanelOpen = !workspaceStore.isPanelOpen trigger immediate updates without explicit subscription boilerplate. This pattern is ideal for optimistic UI updates and local layout state.

Integration Pattern: How SWR Feeds MobX

Plane does not treat these layers as silos; instead, it follows a fetch‑then‑hydrate pattern:

  1. Fetching – SWR retrieves raw JSON from the server via HTTP.
  2. Hydration – A MobX store action (e.g., setWorkspace) receives the fetched payload and writes it into observable fields.
  3. UI Consumption – observer components render the MobX data, while SWR handles background revalidation and cache updates.

This architecture ensures that the UI always reacts to the local MobX state (fast, synchronous updates) while SWR maintains the server‑side source of truth in the background.

Key Files and Locations

Area File Path
SWR Hooks apps/web/core/hooks/use-workspace-issue-properties.ts
SWR Constants packages/constants/src/swr.ts
MobX Stores packages/shared-state/src/store/workspace.store.ts
User Store packages/shared-state/src/store/user.store.ts
Filter Store packages/shared-state/src/store/work-item-filters/filter.store.ts

Summary

  • SWR manages server state in apps/web/core/hooks/, handling remote data fetching, caching, and revalidation via unique fetch keys.
  • MobX manages client state in packages/shared-state/src/store/, providing reactive observables for UI‑centric data through actions and makeObservable.
  • Integration follows a fetch‑then‑hydrate pattern: SWR fetches API data, then MobX stores hydrate observables that components consume via observer.
  • Clear boundaries prevent mixing concerns—SWR never stores local UI flags, and MobX never initiates HTTP requests directly.

Frequently Asked Questions

Why does Plane use both SWR and MobX instead of one library?

SWR excels at caching, deduplication, and background revalidation of remote data, while MobX provides efficient, fine‑grained reactivity for local state mutations. Combining them allows Plane to benefit from SWR’s HTTP‑specific optimizations (stale‑while‑revalidate, focus revalidation) alongside MobX’s synchronous, observable updates for UI interactions.

How does Plane handle optimistic updates with this dual approach?

Optimistic updates mutate the MobX store immediately (e.g., taskStore.updateTaskLocally(task)), giving the user instant feedback. Meanwhile, the SWR mutate function reconciles the change with the server in the background. If the server rejects the update, SWR’s cache can be rolled back and the MobX store updated to reflect the server state.

Where are SWR fetch keys defined in the Plane codebase?

Fetch keys are centralized in packages/constants/src/swr.ts (e.g., WORKSPACE_LABELS, WORKSPACE_CYCLES). This ensures consistent cache keys across multiple hooks and prevents cache fragmentation when different components request the same resource.

Can MobX stores trigger SWR revalidation directly?

No, Plane maintains strict separation: MobX stores do not import SWR or trigger HTTP requests. Instead, components or service layers call SWR’s mutate function after successful API mutations, which updates the global cache and optionally triggers background revalidation. MobX remains focused on local state representation only.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →