How Workspace and Project Views Operate in Plane: MobX State Architecture Explained
Plane resolves workspaces from URL slugs using WorkspaceStore and renders project views through ProjectViewStore, which maintains a reactive cache of view definitions, filters, and favorites in apps/web/core/store.
Plane's open-source project management platform implements a sophisticated state management layer using MobX. The system centers on two primary stores that handle workspace resolution and project view rendering, ensuring reactive updates across the React component tree. This architecture lives in the apps/web directory and manages everything from sidebar navigation preferences to custom view filtering.
The Core State Stores
Plane's UI state is driven by two top-level MobX stores that separate workspace-level concerns from project-specific view management.
WorkspaceStore
The WorkspaceStore (apps/web/core/store/workspace/index.ts) manages the list of workspaces a user belongs to and computes the current active workspace based on the URL. It stores sidebar navigation preferences and project-level navigation settings in observable maps.
Key responsibilities include:
fetchWorkspaces– Retrieves the user's workspace listgetWorkspaceBySlugandgetWorkspaceById– Lookup methods for workspace resolutionfetchSidebarNavigationPreferencesandupdateSidebarPreference– Manage pinned items and orderingfetchProjectNavigationPreferencesandupdateProjectNavigationPreferences– Handle project-specific navigation state
ProjectViewStore
The ProjectViewStore (apps/web/core/store/project-view/store.ts) manages custom project views including list, kanban, calendar, and spreadsheet layouts. It maintains a cache of view definitions and handles filter state management.
Key actions include:
fetchViews– Populates theprojectViewsMapcachegetProjectViewsandgetFilteredProjectViews– Retrieve views with or without filter applicationcreateView,updateView,deleteView– CRUD operationsaddViewToFavoritesandremoveViewFromFavorites– Persistence of user favoritesupdateFiltersandclearAllFilters– Filter state management
Workspace Resolution from URL
Plane determines the active workspace through a reactive dependency chain connecting the router to the store.
Router Integration
The URL contains the workspace slug in the pattern /[workspaceSlug]/…. The router exposes this as router.workspaceSlug, which WorkspaceStore.currentWorkspace observes (lines 48‑53 of apps/web/core/store/workspace/index.ts). When the slug changes, the computed property automatically looks up the matching IWorkspace object from the workspaces map.
Sidebar Navigation Persistence
Navigation preferences are stored per-workspace in navigationPreferencesMap of type IWorkspaceSidebarNavigation. When users pin or reorder sidebar items, the store persists changes via the WorkspaceService API:
// Pinning a sidebar item programmatically
await workspaceStore.updateSidebarPreference(
workspaceSlug,
"issues",
{ is_pinned: true }
);
Project View Lifecycle
Project views follow a strict loading and rendering pattern that ensures data consistency across route changes.
Loading Views on Route Entry
When entering a project route (/[workspaceSlug]/projects/[projectId]), the ProjectWrapper component invokes useProjectView to obtain the store instance and triggers data fetching:
// apps/web/core/layouts/auth-layout/project-wrapper.tsx
const { fetchViews } = useProjectView();
useEffect(() => {
if (workspaceSlug && projectId) fetchViews(workspaceSlug, projectId);
}, [workspaceSlug, projectId]);
This populates the projectViewsMap: Record<string, IProjectView[]> cache, keyed by projectId.
Rendering and Filtering
The ViewsList component reads from the store via the hook and renders ProjectViewListItem components:
// apps/web/core/components/views/views-list.tsx
const { getProjectViews, getFilteredProjectViews, loader } = useProjectView();
const projectViews = getProjectViews(projectId?.toString());
const filteredProjectViews = getFilteredProjectViews(projectId?.toString());
getFilteredProjectViews applies the current filter state stored in the filters object (keyed by view ID) to the raw view array.
CRUD and Filter Operations
The CreateUpdateProjectViewModal handles both creation and updates through the same form component. On submission, it delegates to the store:
// apps/web/core/components/views/modal.tsx
const { createView, updateView } = useProjectView();
const handleFormSubmit = async (values) => {
if (isEdit) await updateView(viewId, values);
else await createView(values);
};
Filter updates propagate through updateFilters in view-list-header.tsx:
// apps/web/core/components/views/view-list-header.tsx
const { filters, updateFilters } = useProjectView();
const onFilterChange = (newExpr) => updateFilters(viewId, newExpr);
Integration Between Workspace and Project Views
The workspace slug scopes every API request in the project view layer. Store methods receive both workspaceSlug and projectId parameters to fetch appropriate data.
When users switch workspaces via the sidebar menu, the router updates router.workspaceSlug. This triggers WorkspaceStore.currentWorkspace to recompute, causing mounted ProjectWrapper components to automatically re-run their useEffect hooks and fetch the new project's view set.
Favorite view handling persists per-workspace through addViewToFavorites and removeViewFromFavorites, ensuring consistent favorites across projects within the same workspace.
Practical Implementation Examples
Accessing the Current Workspace
Use the useWorkspace hook to reactively access workspace data in components:
import { observer } from "mobx-react-lite";
import { useWorkspace } from "@/hooks/store/use-workspace";
const WorkspaceTitle = observer(() => {
const { currentWorkspace } = useWorkspace();
if (!currentWorkspace) return null;
return <h1>{currentWorkspace.name}</h1>;
});
Key file: apps/web/core/hooks/store/use-workspace.ts
Listing Project Views
Fetch and display views with automatic cache updates:
import { observer } from "mobx-react-lite";
import { useProjectView } from "@/hooks/store/use-project-view";
const ProjectViews = observer(({ projectId }: { projectId: string }) => {
const { getProjectViews, fetchViews } = useProjectView();
useEffect(() => {
fetchViews(projectId);
}, [projectId]);
const views = getProjectViews(projectId) ?? [];
return (
<ul>
{views.map((v) => (
<li key={v.id}>{v.name}</li>
))}
</ul>
);
});
Key file: apps/web/core/hooks/store/use-project-view.ts
Programmatic Filter Updates
Apply filters to a specific view using the store's filter API:
import { useProjectView } from "@/hooks/store/use-project-view";
function applyMyFilter(viewId: string) {
const { updateFilters } = useProjectView();
const myFilter = {
operator: "AND",
operands: [{ field: "status", value: "TODO", condition: "is" }],
};
updateFilters(viewId, myFilter);
}
Key file: apps/web/core/store/project-view/store.ts
Summary
- WorkspaceStore (
apps/web/core/store/workspace/index.ts) resolves the current workspace from URL slugs and manages sidebar navigation preferences innavigationPreferencesMap. - ProjectViewStore (
apps/web/core/store/project-view/store.ts) maintains a reactive cache of views inprojectViewsMapand handles CRUD operations, favorites, and filtering. - Route Integration:
ProjectWrappertriggersfetchViewswhen project routes load, whileWorkspaceStoreobservesrouter.workspaceSlugfor automatic workspace switching. - Filter State: Filters are stored per-view-ID in the
filtersobject and applied viagetFilteredProjectViews. - API Scoping: All operations require
workspaceSlugandprojectIdparameters to ensure proper data isolation.
Frequently Asked Questions
How does Plane determine the current active workspace from the URL?
Plane extracts the workspace slug from the URL pattern /[workspaceSlug]/… and exposes it through the router as router.workspaceSlug. The WorkspaceStore.currentWorkspace computed property (lines 48‑53 of apps/web/core/store/workspace/index.ts) observes this value and performs a lookup in the workspaces map to return the matching IWorkspace object. When the URL changes, this reactive dependency automatically updates the active workspace throughout the application.
What is the difference between WorkspaceStore and ProjectViewStore?
WorkspaceStore manages macro-level workspace data including the list of workspaces a user belongs to, the current workspace derived from the URL, and sidebar navigation preferences. ProjectViewStore handles micro-level project view definitions including list, kanban, calendar, and spreadsheet layouts, along with their associated filters and favorites. While WorkspaceStore persists navigation preferences per-workspace, ProjectViewStore caches view definitions in projectViewsMap keyed by projectId.
How are project view filters stored and applied in Plane?
Filters are stored in the ProjectViewStore.filters object using the view ID as the key. When a user modifies filters through the UI (via view-list-header.tsx), the component calls updateFilters(viewId, newExpression). The getFilteredProjectViews(projectId) method then applies these stored filters to the raw view array from projectViewsMap, returning a computed filtered list that automatically updates UI components using MobX observables.
How do favorite views work across different workspaces?
Favorite views are managed through ProjectViewStore.addViewToFavorites and removeViewFromFavorites. These methods persist favorite status per-workspace, meaning a user's favorite view list is consistent across all projects within the same workspace but isolated from other workspaces. The store ensures that favorites are properly scoped when fetching views via fetchViews(workspaceSlug, projectId).
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 →