How State Is Managed in the Plane Frontend Applications
Plane’s frontend applications use a centralized MobX store architecture where a RootStore composes feature-specific stores, exposes them via React context, and drives UI reactivity through the observer pattern, with server-side rendering support via enableStaticRefreshing.
The open-source project management platform makeplane/plane employs a sophisticated state management strategy across its web-based frontends. The main web app, Space app, and Admin app all rely on a hierarchical MobX store pattern that ensures type-safe, reactive state sharing while maintaining clean separation between server and client environments.
The Root Store Architecture
Each Plane application instantiates a central RootStore (or CoreRootStore) class that serves as the single source of truth. This root store composes multiple feature-specific sub-stores such as UserStore, ProjectStore, IssueStore, and CycleStore, wiring them together in its constructor to handle cross-cutting concerns.
The architecture emphasizes explicit lifecycle management. Each root store implements reset or resetOnSignOut methods that recreate the entire store graph when a user logs out, ensuring no stale data persists between sessions.
- Web app root store:
apps/web/core/store/root.store.ts - Space app root store:
apps/space/store/root.store.ts
These files instantiate sub-stores and expose them as public properties, allowing any component that accesses the root store to reach nested state containers.
MobX Integration and React Context
The stores leverage MobX's makeAutoObservable to automatically track state mutations and compute derived values. To support Next.js server-side rendering, Plane invokes enableStaticRendering(true) from mobx-react at the top of each root-store file, preventing memory leaks by ensuring observers do not persist across server requests.
The instantiated RootStore propagates through the component tree via a dedicated React context. The StoreContext is created in apps/web/core/lib/store-context.tsx and supplied by a StoreProvider component that wraps the entire application:
import { StoreProvider } from "@/store-context";
function App() {
return (
<StoreProvider>
<YourRoutesOrComponents />
</StoreProvider>
);
}
This pattern ensures that all stores are singletons within the application scope while remaining testable and resettable.
Accessing State in UI Components
Components consume the store via useContext(StoreContext) and become reactive by wrapping with the observer higher-order component from mobx-react. Because MobX tracks property access at runtime, any observable mutation in a sub-store triggers a targeted re-render only in components that depend on that specific data.
For example, a component reading project data would access the store and destructure observables like this:
import { useContext } from "react";
import { observer } from "mobx-react";
import { StoreContext } from "@/store-context";
const ProjectList = observer(() => {
const { projectRoot } = useContext(StoreContext);
const { projects } = projectRoot; // `projects` is an observable array
return (
<ul>
{projects.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
});
Concrete implementation patterns appear in navigation components, such as apps/web/core/components/navigation/use-tab-preferences.ts, which demonstrates reading and writing MobX state within hooks.
Shared State Across Applications
Plane extracts common state logic into the @plane/shared-state package to avoid duplication between the web and Space applications. This package exports dedicated stores like WorkItemFilterStore and RichFiltersStore from packages/shared-state/src/store/index.ts.
These shared stores follow the same MobX patterns but reside in a separate package, allowing consistent filter logic across different frontend entry points:
import { WorkItemFilterStore } from "@plane/shared-state";
const filterStore = new WorkItemFilterStore();
filterStore.setStatus("open");
filterStore.setAssignee(userId);
// UI components observing `filterStore` update automatically
The specific implementation of work-item filtering resides in packages/shared-state/src/store/work-item-filters/filter.store.ts, which defines observable properties for status, assignees, and other filter criteria.
State Lifecycle: Hydration and Reset
After the server renders the initial HTML, the client hydrates MobX stores with API data through hydrate methods defined in individual stores. This transforms server-fetched JSON into observable class instances.
When a user signs out, the resetOnSignOut method on the root store clears all observables and recreates fresh store instances:
import { store } from "@/store-context";
export function signOut() {
// …perform API logout…
store.resetOnSignOut(); // clears every observable and recreates fresh instances
}
This ensures sensitive project data is purged from memory and the UI returns to a pristine state without requiring a full page reload.
Summary
- Centralized Root Store: Each app uses a
RootStoreclass that instantiates and wires together feature-specific stores likeProjectStoreandIssueStore. - MobX Reactivity: State mutations trigger automatic UI updates via
makeAutoObservableand theobserverHOC, withenableStaticRenderingensuring SSR safety. - Context Provider Pattern: The
StoreContextinapps/web/core/lib/store-context.tsxmakes the root store accessible throughout the component tree. - Cross-App Reusability: The
@plane/shared-statepackage exports stores for common concerns like work-item filters, imported by multiple frontend applications. - Explicit Lifecycle:
hydratemethods seed stores with server data, whileresetOnSignOutguarantees a clean slate on authentication changes.
Frequently Asked Questions
How does Plane handle server-side rendering with MobX?
Plane calls enableStaticRendering(true) from mobx-react at the initialization of each root store file. According to the makeplane/plane source code, this prevents MobX from keeping observers alive during server-side rendering, avoiding memory leaks and ensuring that each HTTP request receives a fresh, isolated store instance.
What is the difference between the web app and Space app store implementations?
Both applications follow the identical architectural pattern of a RootStore composing sub-stores and exposing them via StoreContext. However, they reside in separate directories—apps/web/core/store/root.store.ts versus apps/space/store/root.store.ts—and instantiate different subsets of stores based on their specific feature requirements, while sharing common logic via the @plane/shared-state package.
How do components access and react to state changes?
Components retrieve the store instance via useContext(StoreContext) and wrap their export with the observer function from mobx-react. As implemented in components like those using use-tab-preferences.ts, this combination ensures that the component re-renders automatically whenever any observable property it accesses is mutated, without requiring manual subscription management.
How is state reset when a user signs out?
The root store exposes a resetOnSignOut method—defined in apps/web/core/store/root.store.ts—that recreates all sub-store instances and clears observable state. When the authentication layer calls this method during logout, it guarantees that no cached project data or user preferences persist in memory for the next session.
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 →