How the React Router Layout System Handles Workspace/Project Routing in Plane

Plane implements a nested React Router v6 layout hierarchy using Next.js 13 file-based dynamic segments, where the RouterStore centralized state management extracts workspaceSlug and projectId from URLs and propagates them through MobX observables to child components.

The open-source project management tool Plane (makeplane/plane) combines Next.js 13's App Router with React Router v6 patterns to create a deeply nested, type-safe routing architecture. This system handles complex URL structures like /:workspaceSlug/:projectId/board while maintaining clean separation between workspace-level and project-level UI concerns.

Understanding the Nested Layout Architecture

Plane's routing follows a hierarchical component tree where each level of the URL corresponds to a nested layout file. This React Router layout system creates a parent-child relationship that passes context down through <Outlet /> components.

Root Layout (apps/web/app/layout.tsx)

The entry point renders the top-level <Outlet /> that contains the entire application. Located at apps/web/app/layout.tsx, this file serves as the container for all subsequent nested routes.

Workspace Layout (apps/web/app/[workspaceSlug]/layout.tsx)

Dynamic segments create the first routing boundary. The folder [workspaceSlug] under apps/web/app/ contains a layout.tsx that wraps every workspace-specific page. This layout extracts the workspaceSlug parameter from the URL and initializes the workspace context.

Project Layout (apps/web/app/[workspaceSlug]/[projectId]/layout.tsx)

Inside the workspace folder, another dynamic segment [projectId] adds a second nesting layer. Its layout.tsx renders inside the workspace layout, creating a component tree structure:


<RootLayout>
  <WorkspaceLayout workspaceSlug={...}>
    <ProjectLayout projectId={...}>
      <Outlet />   // page content (e.g., board, issues, settings)
    </ProjectLayout>
  </WorkspaceLayout>
</RootLayout>

Extracting Route Parameters with useAppRouter

Rather than accessing URL parameters directly in components, Plane uses a thin abstraction layer. The useAppRouter hook in apps/web/core/hooks/use-app-router.tsx forwards the Next.js 13 router object:

// apps/web/core/hooks/use-app-router.tsx
export const useAppRouter = () => useRouter();

This hook returns the router instance containing the query object with parsed URL parameters. Layout components call this hook to extract dynamic segments and synchronize them with the global state store.

Centralizing State in the RouterStore

All routing parameters flow into a MobX observable store defined in apps/web/core/store/router.store.ts. The RouterStore watches the Next.js query object and exposes computed getters for commonly used identifiers:

// apps/web/core/store/router.store.ts
export class RouterStore implements IRouterStore {
  query: ParsedUrlQuery = {};

  get workspaceSlug() { return this.query?.workspaceSlug?.toString(); }
  get projectId() { return this.query?.projectId?.toString(); }
  // ... other getters (cycleId, moduleId, etc.)
  
  setQuery(query: ParsedUrlQuery) {
    this.query = query;
  }
}

When the URL changes (e.g., navigating from one project to another), the workspace layout calls routerStore.setQuery(router.query), triggering automatic updates in all observer components subscribed to routerStore.workspaceSlug or routerStore.projectId.

Configuring Client-Side Routing

The file apps/web/react-router.config.ts configures the routing behavior for the development server:

// apps/web/react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
  appDirectory: "app",
  ssr: false,
} satisfies Config;

Setting ssr: false ensures the React Router layout system runs entirely on the client side, allowing the RouterStore to persist and react to navigation events without server interference.

Complete Implementation Example

The following patterns demonstrate how the layouts extract parameters and pass them to child components:

Workspace Layout (apps/web/app/[workspaceSlug]/layout.tsx):

import React from "react";
import { Outlet } from "react-router-dom";
import { useAppRouter } from "@/core/hooks/use-app-router";
import { routerStore } from "@/core/store/router.store";

export default function WorkspaceLayout() {
  const router = useAppRouter();
  
  React.useEffect(() => {
    routerStore.setQuery(router.query);
  }, [router.query]);

  return (
    <main className="workspace">
      <Outlet />
    </main>
  );
}

Project Layout (apps/web/app/[workspaceSlug]/[projectId]/layout.tsx):

import React from "react";
import { Outlet } from "react-router-dom";

export default function ProjectLayout() {
  return (
    <section className="project">
      <Outlet />
    </section>
  );
}

Consuming Store Data in Components:

import { observer } from "mobx-react-lite";
import { routerStore } from "@/core/store/router.store";

const Board = observer(() => {
  const { workspaceSlug, projectId } = routerStore;
  return (
    <div>
      <h1>Board – {workspaceSlug}/{projectId}</h1>
    </div>
  );
});

Summary

  • Plane uses file-based dynamic segments ([workspaceSlug] and [projectId]) to create nested layout boundaries in apps/web/app/.
  • The React Router layout system renders a hierarchy of <Outlet /> components, passing context from root through workspace to project levels.
  • URL parameters are extracted via the useAppRouter hook and centralized in the MobX RouterStore at apps/web/core/store/router.store.ts.
  • Setting ssr: false in apps/web/react-router.config.ts ensures client-side only routing, allowing the store to react to navigation changes.
  • Components access route data through store getters (workspaceSlug, projectId) rather than direct URL parsing, enabling reactive UI updates across the application.

Frequently Asked Questions

How does Plane extract URL parameters from dynamic routes?

Plane extracts URL parameters using the useAppRouter hook in apps/web/core/hooks/use-app-router.tsx, which wraps Next.js 13's useRouter. The layout components call this hook to access the query object, then pass those values to routerStore.setQuery() to synchronize the MobX store with the current URL.

Why does Plane use React Router patterns with Next.js 13?

As implemented in the makeplane/plane repository, Plane uses Next.js 13 for its file-based routing conventions while adopting React Router v6's <Outlet /> component pattern for layout nesting. This hybrid approach leverages Next.js's automatic route generation while maintaining the flexible, component-based layout composition that React Router provides.

What is the purpose of the RouterStore in Plane's routing system?

The RouterStore (apps/web/core/store/router.store.ts) serves as a centralized state container for all URL parameters. It exposes typed getters like workspaceSlug and projectId that any component can subscribe to, eliminating the need to parse URLs repeatedly and ensuring consistent parameter access across the application through MobX's reactive system.

How are the workspace and project layouts nested?

The layouts follow a nested structure where apps/web/app/layout.tsx renders the root <Outlet />, apps/web/app/[workspaceSlug]/layout.tsx renders inside that outlet, and apps/web/app/[workspaceSlug]/[projectId]/layout.tsx nests inside the workspace layout. This creates a three-tier hierarchy where each level provides context to its children while rendering the next <Outlet /> for deeper routes.

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 →