# How React Router Manages SPA Routing with Nested Layouts and Authenticated Routes in Plane

> Learn how Plane uses React Router v6 for SPA routing. Discover nested layouts with Outlet and authentication wrappers for secure access.

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

---

**Plane implements React Router v6+ using a file-based convention where nested layouts compose UI hierarchies through `<Outlet />` components, while authentication wrappers intercept unauthenticated requests and redirect them to the sign-in page.**

Plane's frontend architecture leverages React Router v6+ to deliver a seamless single-page application experience. The routing system follows a file-based convention under `apps/web/app/` that mirrors Remix patterns, where directories containing [`layout.tsx`](https://github.com/makeplane/plane/blob/main/layout.tsx) files automatically become route segments. This structure enables sophisticated nesting of layouts and centralized authentication guards that protect sensitive workspace data.

## File-Based Routing Structure in Plane

The routing hierarchy begins at [`apps/web/app/root.tsx`](https://github.com/makeplane/plane/blob/main/apps/web/app/root.tsx), which serves as the global entry point for the React Router tree. This file renders the standard Remix-style components including `Links`, `Meta`, and `Scripts`, while providing an `<Outlet />` where the entire route tree renders.

```tsx
// apps/web/app/root.tsx
import { Links, Meta, Outlet, Scripts } from "react-router";

export default function Root() {
  return (
    <>
      <Links />
      <Meta />
      <Outlet />
      <Scripts />
    </>
  );
}

```

Each folder under `apps/web/app/` that contains a [`layout.tsx`](https://github.com/makeplane/plane/blob/main/layout.tsx) (or `+page.tsx`, `+loader.tsx`) becomes a distinct route segment. This convention eliminates manual route configuration, allowing the file system itself to define the URL structure.

## Implementing Nested Layouts with Outlet Components

Plane achieves deep UI nesting through React Router's `<Outlet />` pattern, where parent layouts render placeholders for child routes without duplicating chrome or logic.

### Root Layout and Resource Preloading

The top-level layout at `apps/web/app/(all)/layout.tsx` preloads critical resources and establishes the application shell that persists across all routes.

```tsx
// apps/web/app/(all)/layout.tsx
import { Outlet } from "react-router";
import { PreloadResources } from "./layout.preload";

export default function AppLayout() {
  return (
    <>
      <PreloadResources />
      <Outlet />
    </>
  );
}

```

This pattern ensures that authentication checks, global navigation, and resource loading occur once at the appropriate level, while specific page content renders within the nested outlet.

### Workspace-Level Layouts and Parameter Capture

Dynamic segments like `workspaceSlug` are captured through React Router's `useParams` hook within layout components. The workspace layout at `apps/web/app/(all)/[workspaceSlug]/layout.tsx` extracts URL parameters and makes them available to all nested children.

```tsx
// apps/web/app/(all)/[workspaceSlug]/layout.tsx
import { Outlet, useParams } from "react-router";

export default function WorkspaceLayout() {
  const { workspaceSlug } = useParams();   // ← URL param
  // …maybe fetch workspace data here…
  return <Outlet />;
}

```

### Deep Nesting for Settings Routes

The settings section demonstrates tertiary nesting at `apps/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx`. This layout wraps settings-specific pages like `/settings/profile` while inheriting both the global and workspace-level parent layouts.

```tsx
// apps/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx
import { Outlet } from "react-router";

export default function SettingsLayout() {
  return (
    <section className="settings">
      {/* Shared settings UI */}
      <Outlet />   {/* Renders /settings/profile, /settings/security, … */}
    </section>
  );
}

```

## Protecting Routes with Authentication Wrappers

Plane centralizes route protection through custom authentication wrappers located in `apps/web/core/layouts/auth-layout/`. These components intercept rendering and either display the nested route or redirect unauthenticated users.

The `WorkspaceAuthWrapper` inspects session state via `useAuth` and triggers a React Router `redirect` when authentication is missing.

```tsx
// apps/web/core/layouts/auth-layout/workspace-wrapper.tsx
import { redirect } from "react-router";
import { useEffect } from "react";
import { useAuth } from "@/hooks/use-auth";

export default function WorkspaceAuthWrapper({ children }: { children: ReactNode }) {
  const { isAuthenticated, loading } = useAuth();

  useEffect(() => {
    if (!loading && !isAuthenticated) {
      // React Router v6 redirect
      redirect("/signin");
    }
  }, [isAuthenticated, loading]);

  // While checking auth we can show a spinner
  if (loading) return <Spinner />;

  return <>{children}</>;
}

```

Layout files consume these wrappers to protect entire route branches. For example, `apps/web/app/(all)/[workspaceSlug]/layout.tsx` wraps its outlet with the authentication component:

```tsx
// apps/web/app/(all)/[workspaceSlug]/layout.tsx
import WorkspaceAuthWrapper from "@/core/layouts/auth-layout/workspace-wrapper";

export default function WorkspaceLayout() {
  return (
    <WorkspaceAuthWrapper>
      <Outlet />   {/* Only visible when the user can access the project */}
    </WorkspaceAuthWrapper>
  );
}

```

## Handling Redirects and Legacy URLs

Plane maintains backward compatibility through dedicated redirect routes in `apps/web/app/routes/redirects/core/`. These files utilize React Router's server-side `redirect` helper to map legacy URLs to the current route structure.

```tsx
// apps/web/app/routes/redirects/core/signin.tsx
import { redirect } from "react-router";

export default function loader() {
  return redirect("/auth/signin");
}

```

This pattern ensures that bookmarks and external links to `/signin` seamlessly resolve to `/auth/signin` without requiring client-side JavaScript execution.

## Navigation Hooks and Programmatic Routing

Individual components leverage React Router's navigation hooks for dynamic behavior. The `useParams` hook extracts URL segments for breadcrumb generation, while `useNavigate` enables programmatic navigation after asynchronous operations like login.

```tsx
import { useParams, Link } from "react-router";

export function WorkspaceBreadcrumb() {
  const { workspaceSlug } = useParams();   // “my‑workspace”
  return <Link to={`/workspace/${workspaceSlug}`}>Workspace</Link>;
}

```

For post-login redirection, components use `useNavigate` to transition client-side:

```tsx
import { useNavigate } from "react-router";

function LoginForm() {
  const navigate = useNavigate();

  async function handleSubmit(e) {
    e.preventDefault();
    const success = await authService.login(...);
    if (success) navigate("/dashboard");   // client‑side navigation
  }

  return <form onSubmit={handleSubmit}>/* … */</form>;
}

```

Programmatic navigation appears throughout the navigation components, such as [`apps/web/core/components/navigation/tab-navigation-root.tsx`](https://github.com/makeplane/plane/blob/main/apps/web/core/components/navigation/tab-navigation-root.tsx), which uses `useLocation` to determine active states.

## Summary

- **Plane uses React Router v6+** with a file-based routing convention under `apps/web/app/` where folders containing [`layout.tsx`](https://github.com/makeplane/plane/blob/main/layout.tsx) automatically become route segments.
- **Nested layouts rely on `<Outlet />` components** to compose UI hierarchies, allowing parent routes to provide chrome and data loading while children render specific content.
- **Authentication wrappers** in `apps/web/core/layouts/auth-layout/` protect route branches by checking `useAuth` state and triggering `redirect` for unauthenticated users.
- **Dynamic parameters** like `workspaceSlug` are captured via `useParams` in layout components and propagated to nested routes.
- **Legacy URL support** is implemented through server-side redirect routes that map old paths to new locations using React Router's `redirect` helper.

## Frequently Asked Questions

### How does Plane handle authentication redirects in React Router?

Plane handles authentication redirects through wrapper components that wrap the `<Outlet />` in protected layouts. The `WorkspaceAuthWrapper` checks authentication status via the `useAuth` hook, and if the user is unauthenticated, it triggers React Router's `redirect` function to navigate to `/signin`. This ensures that entire route branches are protected without adding authentication logic to individual page components.

### What file convention does Plane use for nested routes?

Plane follows a file-based routing convention similar to Remix, where the directory structure under `apps/web/app/` defines the URL hierarchy. Any folder containing a [`layout.tsx`](https://github.com/makeplane/plane/blob/main/layout.tsx) file becomes a route segment, with the layout rendering an `<Outlet />` where child routes appear. Dynamic segments use bracket notation like `[workspaceSlug]`, and the layout captures these parameters using `useParams`.

### Can layouts in Plane fetch data before rendering children?

Yes, layouts in Plane can fetch data by utilizing React Router's data loading patterns. Layout components can access URL parameters via `useParams` and initiate data fetching (such as workspace metadata) before rendering the `<Outlet />`. While the analysis shows `useEffect` patterns for authentication checks, the file-based structure supports Remix-style loaders in `+loader.tsx` files that can preload data for the route tree.

### How does Plane manage programmatic navigation after actions like login?

Plane uses the `useNavigate` hook from React Router v6+ to handle programmatic navigation. After successful authentication or form submissions, components call `navigate("/dashboard")` or similar paths to transition between routes client-side. This approach prevents full page reloads while updating the URL and rendering the appropriate nested layout hierarchy.