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

> Discover how Plane's React Router v6 layout system uses Next.js dynamic segments to manage workspace and project routing, centralizing state with RouterStore for seamless navigation.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: internals
- Published: 2026-06-22

---

**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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/apps/web/core/hooks/use-app-router.tsx) forwards the Next.js 13 router object:

```tsx
// 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`](https://github.com/makeplane/plane/blob/main/apps/web/core/store/router.store.ts). The `RouterStore` watches the Next.js query object and exposes computed getters for commonly used identifiers:

```ts
// 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`](https://github.com/makeplane/plane/blob/main/apps/web/react-router.config.ts) configures the routing behavior for the development server:

```ts
// 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`):**

```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`):**

```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:**

```tsx
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`](https://github.com/makeplane/plane/blob/main/apps/web/core/store/router.store.ts).
- Setting `ssr: false` in [`apps/web/react-router.config.ts`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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.