Back to React
React
medium
mid

How would you implement route based code splitting with role based access control in a React app?

Wrap routes with an `<RbacRoute>` that checks user role before rendering. Lazy-import the protected component inside that check so unauthorized users never download it. Server should also enforce RBAC (don't trust the client). For finer control, gate per feature with role-aware lazy imports and a permission hook (`usePermission(role)`).

4 min read·~15 min to think through

Route + role gate + lazy

tsx
import { Routes, Route, Navigate } from "react-router-dom";

const AdminPanel = React.lazy(() => import("./pages/AdminPanel"));
const BillingPage = React.lazy(() => import("./pages/BillingPage"));

function RbacRoute({ role, children }: { role: string; children: ReactNode }) {
  const { user, isLoading } = useAuth();
  if (isLoading) return <Skeleton />;
  if (!user) return <Navigate to="/login" replace />;
  if (!user.roles.includes(role)) return <Forbidden />;
  return <Suspense fallback={<Skeleton />}>{children}</Suspense>;
}

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/admin" element={<RbacRoute role="admin"><AdminPanel /></RbacRoute>} />
  <Route path="/billing" element={<RbacRoute role="billing"><BillingPage /></RbacRoute>} />
</Routes>

AdminPanel chunk doesn't download until <RbacRoute> confirms the role.

Server-side enforcement

Always enforce RBAC on the server too. The client gate prevents UI access; the server gate prevents data leakage. A user could bundle-spelunk into the admin code, but they can't bypass server auth.

Permission hook for finer control

tsx
function usePermission(perm: string) {
  const { user } = useAuth();
  return user?.permissions.includes(perm) ?? false;
}

function DeleteButton() {
  if (!usePermission("user.delete")) return null;
  return <button>Delete</button>;
}

For component-level RBAC inside otherwise-shared pages.

Lazy + permission

tsx
function MaybeBillingWidget() {
  const canBill = usePermission("billing.read");
  if (!canBill) return null;
  // Only authorized users download this chunk
  const BillingWidget = React.useMemo(() => React.lazy(() => import("./BillingWidget")), []);
  return <Suspense fallback={<Skeleton />}><BillingWidget /></Suspense>;
}

Server-driven splitting (advanced)

For strict isolation:

  • Serve a different bundle manifest per role.
  • Webpack/rspack code splitting + a routing layer that picks the bundle.
  • Defense in depth — the admin chunk simply isn't served to non-admins.

Rare; most apps trust client-side enforcement + server data gates.

Route loaders + auth

In data routers (React Router 6.4+, TanStack Router, Next.js App Router):

tsx
{
  path: "/admin",
  loader: async () => {
    const user = await getUser();
    if (!user.roles.includes("admin")) throw redirect("/forbidden");
    return null;
  },
  lazy: () => import("./AdminPanel"),
}

Loader runs before the lazy chunk is requested; one round trip to validate.

Prefetch only for authorized users

tsx
<Link
  to="/admin"
  onMouseEnter={user?.roles.includes("admin") ? () => import("./AdminPanel") : undefined}
>
  Admin
</Link>

Avoids hint-leaking the existence of admin chunks to unauthorized users (though the URL itself can still be inspected).

Anti-patterns

  • Trusting client-side roles for security.
  • Bundling all admin code into the main bundle to "simplify."
  • Showing hidden routes in nav for forbidden roles.
  • Mismatched server/client permission lists.

Interview framing

"<RbacRoute> wrapper that runs the auth + role check before rendering the lazy-imported page. Unauthorized users don't download the chunk. Lazy-import via React.lazy + <Suspense>. Combine with a usePermission hook for component-level gates inside shared pages. ALWAYS enforce RBAC on the server — client gates are UX, not security. In data routers, use route loaders to validate before the chunk loads, saving a render cycle. For strict isolation, serve different bundle manifests per role — but most apps trust client gates + server data enforcement. Don't prefetch admin chunks for non-admin users."

Follow-up questions

  • Why isn't client-side RBAC enough?
  • How do data router loaders fit in?
  • How would you handle role changes mid-session?

Common mistakes

  • Trusting client-side roles for security.
  • Bundling protected code into main bundle.
  • No server-side enforcement.

Performance considerations

  • Authorized users save the route bundle until they hit it. Loaders avoid render-then-redirect waste.

Edge cases

  • Role changes after login (re-check on navigation).
  • Multi-tenant role scopes.
  • Anonymous + role-gated content.

Real-world examples

  • Auth0 React Router patterns, Next.js middleware RBAC, internal admin tools.

Senior engineer discussion

Seniors separate UX gating from security gating, enforce server-side, and design loaders that fail fast before paying for chunk loads.

Related questions