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

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 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, 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.

// 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 (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.

// 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.

// 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.

// 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.

// 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:

// 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.

// 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.

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.

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:

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, 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 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 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.

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 →