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)`).
Route + role gate + lazy
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
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
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):
{
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
<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.