Routing — Cheat Sheet
React · 5 topics. Download the PDF or the Instagram carousel and share it.
React Router — Routes, Links and Layouts
Routes map URLs to components, Link navigates without a reload, and nested routes let a layout stay mounted while only the inner part changes.
- ✓Nested routes keep a layout mounted while only the inner Outlet changes on navigation
- ✓Link and NavLink navigate client-side; a plain anchor to an internal route reloads the whole app
- ✓navigate(path, { replace: true }) avoids a history entry, which matters after a redirect
- ✓Always define a catch-all route so an unknown URL renders a real 404 page
- ✓Loaders fetch during the transition rather than after render, and errorElement contains failures to one subtree
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />, // header, nav, footer — stays mounted
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home /> },
{ path: 'problems', element: <ProblemList /> },
{ path: 'problems/:slug', element: <ProblemDetail /> },
{
path: 'account',
element: <AccountLayout />, // a nested layout with its own tabs
children: [
{ index: true, element: <Profile /> },
{ path: 'billing', element: <Billing /> },
],
},
{ path: '*', element: <NotFound /> }, // 404, always include one
],
},
])
<RouterProvider router={router} />
// The layout renders <Outlet /> where the child route goes
function RootLayout() {
return <><Header /><main><Outlet /></main><Footer /></>
}Route Params and Search Params as State
Path params identify a resource, search params describe a view. Treating search params as your filter state makes every view shareable and refresh-proof for free.
- ✓Path params identify a resource; search params describe how the view is filtered or sorted
- ✓Params arrive as strings and are user input — parse, validate and handle the invalid case
- ✓Preserve other search params when setting one, and reset the page whenever a filter changes
- ✓Use replace for filter adjustments so the back button leaves the page rather than undoing ten tweaks
- ✓Never put tokens or personal data in a query string — it is logged, cached and sent in referrers
{ path: 'problems/:slug' }
const { slug } = useParams() // always a string, never typed
// The URL is user input. Anyone can type /problems/../../etc
const { data, error } = useQuery({
queryKey: ['problem', slug],
queryFn: () => getProblem(slug),
})
if (error?.status === 404) return <NotFound />
// Numeric ids need parsing and checking
const id = Number(useParams().id)
if (!Number.isInteger(id)) return <NotFound />
// Optional and catch-all segments
{ path: 'problems/:slug?' }
{ path: 'docs/*' } // useParams()['*'] holds the rest
// A changing param does NOT remount the component by default —
// the same route matched, so state persists across the navigation.
// Key on it when a fresh start is what you want:
<ProblemDetail key={slug} />Protected Routes and Authorisation
A route guard is a UX affordance, not a security boundary. It hides what the user cannot use and sends them somewhere sensible — the server still decides what they may actually do.
- ✓A route guard is a UX affordance — the API must enforce the same rule, because the bundle is public
- ✓Check the auth "checking" state first, or the guard redirects signed-in users on every reload
- ✓Redirect with replace and carry the intended destination so sign-in returns the user where they were going
- ✓Signed out means redirect to sign in; signed in but not permitted means show a forbidden or upgrade screen
- ✓Prompt to reauthenticate in place rather than navigating away from a form with unsaved input
function RequireAuth() {
const { user, isChecking } = useAuth()
const location = useLocation()
if (isChecking) return <FullPageSpinner /> // not signed out — unknown
if (!user) {
return <Navigate to="/login" replace state={{ from: location }} />
}
return <Outlet />
}
// Wrap the protected subtree
{
element: <RequireAuth />,
children: [
{ path: 'dashboard', element: <Dashboard /> },
{ path: 'account', element: <Account /> },
],
}
// Return the user where they were going after sign-in
const from = location.state?.from?.pathname ?? '/dashboard'
navigate(from, { replace: true })
// replace on the redirect, or back returns them to the guard,
// which redirects again — a loop the user cannot escape.Lazy Routes, Suspense and Loading Transitions
Splitting by route is the highest-value code splitting there is: a visitor to the home page should not download the admin panel or the code editor.
- ✓Route-level splitting is the highest-value split — visitors do not download features they never open
- ✓Place the Suspense boundary inside the layout so the shell does not flash on every transition
- ✓Prefetch a chunk on hover or focus so the navigation itself feels instant
- ✓useTransition keeps the current page interactive instead of dropping immediately to a fallback
- ✓After a deploy, old chunks disappear and lazy imports fail — catch that error and offer a reload
const Admin = lazy(() => import('./features/admin/AdminPage'))
const Editor = lazy(() => import('./features/editor/EditorPage'))
<Route
path="/admin"
element={
<Suspense fallback={<PageSkeleton />}>
<Admin />
</Suspense>
}
/>
// Put the Suspense boundary inside the layout, not around it —
// otherwise the header and nav disappear during every transition
// and the whole page flashes.
// Worth splitting: admin areas, the code editor, chart libraries,
// rich text editors, PDF generation, anything most visitors never open.
// Not worth splitting: small components on the critical path — an
// extra request costs more than the bytes saved.
// With React Router's own lazy, the route module itself is split,
// loader included:
{ path: 'admin', lazy: () => import('./routes/admin') }Navigation Patterns — Scroll, Modals and 404s
The details that separate a single-page app from a website that feels right: where the page scrolls to, what the back button does to a modal, and what an unknown URL renders.
- ✓Scroll to top on a push navigation but let the browser restore position on back and forward
- ✓Move focus to the main heading after navigation, or screen-reader users are left behind
- ✓An overlay users might share or expect back to close belongs in the URL; an ephemeral one belongs in state
- ✓Distinguish a missing route from missing data — they need different pages and different words
- ✓A client-side route 404s on refresh unless the server returns index.html for unknown paths
// Without this, navigating from halfway down a list leaves the new
// page scrolled halfway down.
function ScrollToTop() {
const { pathname, hash } = useLocation()
const navigationType = useNavigationType()
useEffect(() => {
if (navigationType === 'POP') return // back/forward: let the
// browser restore position
if (hash) {
document.querySelector(hash)?.scrollIntoView()
return
}
window.scrollTo(0, 0)
}, [pathname, hash, navigationType])
return null
}
// React Router's <ScrollRestoration /> does this, including saving
// and restoring positions per history entry.
// Focus matters as much as scroll: after navigation, move focus to
// the main heading, or a screen-reader user stays where they were.
useEffect(() => { headingRef.current?.focus() }, [pathname])