How the Plane React Frontend Manages Global State Using MobX Stores
The Plane React frontend centralizes global state in a single CoreRootStore that aggregates MobX stores from the @plane/shared-state package, providing them to components via React Context while using the observer pattern from mobx-react for fine-grained reactivity.
The open-source project management tool Plane (makeplane/plane) implements a scalable state management architecture for its Next.js React frontend. By leveraging MobX observable stores within a dedicated @plane/shared-state workspace package, the application achieves type-safe, modular global state that eliminates prop-drilling and optimizes re-renders across the component tree.
The @plane/shared-state Package Structure
The @plane/shared-state package is a workspace-level module that houses reusable MobX stores for data requiring cross-cutting UI access, such as work-item filters and rich-filter instances. This package acts as a centralized source for complex state logic that must remain agnostic of specific view implementations.
The package's entry point re-exports all store utilities from its internal store directory:
// packages/shared-state/src/index.ts
export * from "./store";
export * from "./utils";
This structure allows the web application to import specific store classes and types without referencing internal file paths, maintaining clean dependency boundaries across the monorepo.
MobX Store Architecture and Patterns
Each store within the shared-state package follows a consistent three-part MobX pattern: observable state, actions, and computed functions.
In packages/shared-state/src/store/work-item-filters/filter.store.ts, the WorkItemFilterStore class demonstrates this architecture:
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,
resetExpression: action,
// …
});
}
// Computed getter – memoised with MobX‑utils
getFilter = computedFn((entityType, entityId) =>
this.filters.get(this._getFilterKey(entityType, entityId))
);
// …
}
The makeObservable call explicitly defines which properties are observable and which methods are actions, ensuring strict mutation tracking. Computed values leverage computedFn from mobx-utils to maintain referential stability and prevent unnecessary re-renders in consuming components.
The Root Store Pattern: Aggregating Global State
The web application creates a single CoreRootStore instance that aggregates every domain-specific store, including those imported from the shared-state package. This singleton pattern ensures all global mutable state exists within one cohesive object graph.
In apps/web/core/store/root.store.ts, the root store instantiates the shared WorkItemFilterStore:
import { WorkItemFilterStore } from "@plane/shared-state";
export class CoreRootStore {
// …many other stores
workItemFilters: IWorkItemFilterStore;
constructor() {
// …initialize other stores
this.workItemFilters = new WorkItemFilterStore(); // <-- shared‑state store
}
}
This aggregation pattern allows the application to initialize all global state at application startup, ensuring consistent state availability throughout the component lifecycle.
Providing Stores via React Context
To eliminate prop-drilling, Plane implements a React Context that holds the CoreRootStore instance. The StoreContext in apps/web/core/lib/store-context.tsx creates a global reference accessible to any component in the tree:
import { createContext } from "react";
import { RootStore } from "@/plane-web/store/root.store";
export let rootStore = new RootStore();
export const StoreContext = createContext<RootStore>(rootStore);
export function StoreProvider({ children }: { children: ReactElement }) {
return <StoreContext.Provider value={rootStore}>{children}</StoreContext.Provider>;
}
The StoreProvider wraps the entire application in apps/web/app/provider.tsx, ensuring every component can access the root store via useContext(StoreContext). For server-side rendering compatibility, the implementation uses enableStaticRendering to disable MobX reactions on the server, preventing memory leaks during Next.js SSR hydration.
Consuming Stores in React Components
Components access specific stores by destructuring from the root store context. To maintain reactive updates, components that read observable state must be wrapped with the observer HOC from mobx-react.
For example, in apps/web/core/components/work-item-filters/filters-row.tsx:
import { observer } from "mobx-react";
import type { IWorkItemFilterInstance } from "@plane/shared-state";
type Props = {
filter: IWorkItemFilterInstance;
// …
};
export const WorkItemFiltersRow = observer(function WorkItemFiltersRow({ filter, ...rest }: Props) {
return <FiltersRow filter={filter} {...rest} />;
});
The observer wrapper ensures that any changes to the filter instance's observable properties automatically trigger component re-renders. Components typically access stores through custom hooks that abstract the context consumption:
function useWorkItemFilter(entityType, entityId) {
const { workItemFilters } = useContext(StoreContext);
const filter = workItemFilters.getOrCreateFilter({
entityType,
entityId,
showOnMount: true,
onExpressionChange: (expr) => console.log("new expression", expr),
});
return filter;
}
Summary
- Centralized State: All global state resides in a single
CoreRootStorethat aggregates domain-specific MobX stores, including those from@plane/shared-state. - Explicit Observability: Stores use
makeObservableto declare observable properties and actions, whilecomputedFnmemoizes derived values. - Context Distribution: The
StoreContextReact context eliminates prop-drilling by providing the root store to any component in the tree. - Reactive Components: The
observerHOC frommobx-reactbinds components to observable state changes, enabling fine-grained re-renders only when accessed data mutates. - SSR Safety:
enableStaticRenderingensures MobX reactions are disabled during server-side rendering, preventing memory leaks in Next.js applications.
Frequently Asked Questions
How does the React frontend access MobX stores from the shared-state package?
The React frontend accesses MobX stores through a centralized CoreRootStore instance that aggregates all global stores, including those from @plane/shared-state. This root store is provided to the component tree via React Context (StoreContext), allowing any component to import the context and access specific stores like workItemFilters using the useContext hook.
What is the purpose of the observer wrapper in Plane's React components?
The observer wrapper from mobx-react transforms React components into reactive observers that automatically re-render when MobX observable properties change. Without this wrapper, components would read initial state values but fail to update when mutations occur, breaking the reactive data flow between the stores and the UI.
Why does Plane use computedFn instead of standard MobX computed decorators?
Plane uses computedFn from mobx-utils for parameterized computations that require memoization, such as the getFilter method in WorkItemFilterStore. Standard computed decorators work best for property getters, while computedFn optimizes method calls with arguments by caching results based on input parameters, preventing unnecessary recalculations and re-renders.
How does the shared-state architecture support server-side rendering?
The architecture supports SSR through enableStaticRendering, which disables MobX's reactive tracking system during server-side execution. This prevents the accumulation of reaction disposables that could cause memory leaks during Next.js server-side rendering. The same root store structure works on both server and client, with reactions only activating once hydrated in the browser.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →