React Router Cheatsheet - Routes, Links & Params

This reference is for React developers wiring up client-side navigation with React Router v6. It covers the declarative Routes/Route tree, nested layouts with Outlet, Link/NavLink and programmatic useNavigate, reading dynamic params and refreshing the query string, guarding protected pages, and the reactive data APIs (loader/action) added in v6.4. Each entry fills a concrete gap, like rendering a 404, keeping nav active state, or redirecting after login. After reading you should be able to declare a route tree, link between pages, handle :id params and ?query, and protect authenticated screens.

Languages·36 commands·Last updated 2026-07-21
reactrouterRoutingnavigation

Typical Use Case

For front-end developers who need URL state in a single-page application: page transitions, auth guards, shareable links with parameters, and "back to the same screen after a refresh". React Router makes the URL the single source of truth — filters live in search params, the current entity lives in a path parameter (:id), and cross-page return state lives in the navigate state. This sheet covers v6 Routes / Route / Link / NavLink / Navigate, useParams / useSearchParams / useLocation / useMatch, nested routes with Outlet, lazy loading with Suspense, and the v6.4+ data loading APIs (loader / action / errorElement).

Router Setup 5

<BrowserRouter><App /></BrowserRouter>
Wrap the app to enable routing
<Routes><Route path="/" element={<Home />} /></Routes>
Declare route rules
<Route path="/users/:id" element={<User />} />
Dynamic route param
<Route path="*" element={<NotFound />} />
404 fallback route
<Route path="/" element={<Layout />}>...</Route>
Root route with a layout

Route Params 5

<Route path="/users/:id" element={<User />} />
Declare a dynamic param
const { id } = useParams()
Get the dynamic params object
<Route path="/files/*" element={<Files />} />
Match multiple path segments (splat)
const splat = useParams()["*"]
Get the splat segment
navigate(-1) / navigate(1)
Go back / forward one page

Query Params 5

const [params, setParams] = useSearchParams()
Read/write query params
params.get("q")
Read a single query param
setParams({ q: "keyword", page: 1 })
Update query params
<Link to="/search?q=react">Search</Link>
Link with a query string
params.toString()
Serialize to a query string

Nested & Layout 5

<Route path="/dashboard" element={<Dashboard />}>
Parent route
<Route index element={<Overview />} />
Default child route (index route)
<Route path="settings" element={<Settings />} />
Relative-path child route
<Outlet />
Render child routes in the parent
<Outlet context={{ user }} />
Pass context to child routes

Guards & Data 5

function ProtectedRoute({ children }) { return isAuth ? children : <Navigate to="/login" /> }
Custom protected route component
<Route element={<ProtectedRoute><Outlet /></ProtectedRoute>}>
Wrap multiple protected routes
<Route loader={async ({ params }) => fetchUser(params.id)} />
Data loader (v6.4+)
<Route action={async ({ request }) => submitForm(request)} />
Data action (v6.4+)
<Route errorElement={<ErrorBoundary />} />
Route-level error boundary

Advanced 6

const location = useLocation()
Get the location object
location.pathname / location.search / location.hash
Parse the current URL parts
const match = useMatch("/users/:id")
Match a path pattern
const Page = lazy(() => import("./Page"))
Lazy-load a component
<Route path="about" element={<Suspense><About /></Suspense>} />
Route-level lazy loading
<ScrollRestoration />
Scroll position restoration (v6.4+)

Parameter matrix

ParameterEffectExample
<Route path="/users/:id" element={<User />} />Dynamic path parameter: the matching URL segment is captured and read via useParams()const { id } = useParams()
<Route path="*" element={<NotFound />} />Catch-all route matching anything the earlier routes did not<Route path="*" element={<NotFound />} />
navigate(path, { replace, state })Programmatic navigation with history replacement and carried statenavigate("/dashboard", { replace: true, state: { from: "/" } })
<NavLink className={({ isActive }) => ...}>Link variant that passes isActive to a callback for styling<NavLink to="/users" className={({ isActive }) => isActive ? "active" : ""}>
useSearchParams()Read and write the query string, returning [params, setParams]const [params, setParams] = useSearchParams()
<Outlet /> / <Outlet context={...} />Parent renders the child route and can pass it a context value<Outlet context={{ user }} />
<Route index element={<Overview />} />Default child route of a parent path<Route index element={<Overview />} />
loader / action / errorElementv6.4+ route-level data loading, submission and error boundary<Route loader={async ({ params }) => fetchUser(params.id)} />
path="/files/*"Splat route matching multiple segments, read via useParams()["*"]const splat = useParams()["*"]

Common pitfalls

SymptomIn a nested route layout the shell renders but the content area is empty.

CauseThe parent component is missing <Outlet />, so child route elements have no render outlet.

FixPlace <Outlet /> in the layout the parent returns; to pass data down, use <Outlet context={{ user }} />.

SymptomVisiting /dashboard/settings returns a 404 or a blank page.

CauseThe child route was written as an absolute path starting with a slash, or the parent element does not wrap it.

FixWrap children with element={<Dashboard />} on the parent and use a relative path: <Route path="settings" element={<Settings />} />.

SymptomClicking a link triggers a full page reload and a new document navigation in the console.

CauseA native <a href="/about"> was used instead of <Link to="/about">.

FixSwitch to <Link to="/about">About</Link>, which renders an anchor but lets the router handle the click.

SymptomAfter login the intended page is lost, or navigating back from a detail page clears the list state.

Causenavigate(path) pushed a new history entry, or the state is gone after refresh since navigate state lives only in memory.

FixUse replace for the login redirect: navigate("/dashboard", { replace: true, state: { from: "/" } }); for refresh-safe restoration put the origin in a search parameter instead of state.

SymptomOne filter change via useSearchParams leaves a long trail of duplicated parameters in the URL.

CauseEach call pushed instead of replacing, or setParams ran repeatedly inside an effect.

FixPass the full parameter object in one call — setParams({ q: "keyword", page: 1 }) — and only call it once dependencies settle.

SymptomThe 404 page never appears and unmatched paths render blank.

Cause<Route path="*" /> was declared before other routes, and the first match wins in declaration order.

FixMove the catch-all <Route path="*" element={<NotFound />} /> to the last position inside Routes.

Troubleshooting

  1. 1See which path pattern the current location matches

    const match = useMatch("/users/:id")

    A null result means the pattern did not match, which is the fastest way to verify splat and dynamic segment syntax.

  2. 2Break the current URL into its parts

    const location = useLocation()

    location.pathname / location.search / location.hash give path, query string and hash; log these first when a URL is malformed.

  3. 3Gate routes that require authentication

    <Route element={<ProtectedRoute><Outlet /></ProtectedRoute>}>

    ProtectedRoute returns <Navigate to="/login" /> when unauthenticated; the wrapper form protects many routes at once.

  4. 4Give route-level data loading a fallback UI

    <Route errorElement={<ErrorBoundary />} />

    Thrown loader / action errors render errorElement instead of bubbling up to a blank page.

  5. 5Lazy-load a single route

    <Route path="about" element={<Suspense><About /></Suspense>} />

    Pair it with const Page = lazy(() => import("./Page")) so the whole app is not bundled up front.

Command Examples

Declarative routes and the catch-all

<BrowserRouter><App /></BrowserRouter> <Routes> <Route path="/" element={<Home />} /> <Route path="/users/:id" element={<User />} /> <Route path="*" element={<NotFound />} /> </Routes>

path="*" must sit at the end of Routes to actually catch unmatched paths, and the dynamic :id segment is read back with useParams().

Nested routes and the Outlet

<Route path="/dashboard" element={<Dashboard />}> <Route index element={<Overview />} /> <Route path="settings" element={<Settings />} /> </Route> // inside Dashboard: <Outlet /> <Outlet context={{ user }} />

The index route matches /dashboard itself, and without an Outlet in the parent a matched child has nowhere to render.

Splitting query params from path params

const [params, setParams] = useSearchParams() params.get("q") setParams({ q: "keyword", page: 1 }) params.toString() const { id } = useParams() const splat = useParams()["*"]

Filters belong in the query string and the entity id in the path; the tail of a splat route such as /files/* comes from the star key.

Programmatic navigation and guarded routes

const navigate = useNavigate() navigate("/dashboard", { replace: true, state: { from: "/" } }) navigate(-1) navigate(1) <Route element={<ProtectedRoute><Outlet /></ProtectedRoute>}> <Route path="secret" element={<Secret />} /> </Route>

replace keeps a forbidden page out of history, while state carries the origin so the user can be returned there after signing in.

Common Pitfalls

  • Forgetting <Outlet /> in the parent means a matched child renders nothing while the parent layout looks perfectly fine.
  • An absolute path in a child route escapes the parent relationship, so stick to relative segments such as path="settings" inside a nested tree.
  • Navigate with <Link> or navigate, never a plain anchor: a raw href reloads the whole document and shows up as a full page navigation.
  • Always add replace when redirecting to the login screen, otherwise the guarded route stays in history and the user is bounced again after signing in.
  • An unmatched path needs <Route path="*" element={<NotFound />} /> as the fallback, and that route has to be last inside Routes.

Tips

  • React Router v6 replaces v5's <Switch> with <Routes>.
  • useNavigate replaces v5's useHistory: navigate(-1) goes back, navigate(1) goes forward.
  • Nested routes need <Outlet /> in the parent to render children; an index route matches the parent path itself.
  • NavLink's className accepts an { isActive } callback for active styling.
  • Get dynamic params :id with useParams(); read query params ?q= with useSearchParams().

FAQ

The parent layout renders but the child area is blank. What is missing?

An <Outlet />. After the parent renders its layout you must place <Outlet /> inside it as the mount point for the child route; otherwise the child element matches successfully but has nowhere to render. The parent route itself also needs the nested form <Route path="/dashboard" element={<Dashboard />}> with child Routes. There is usually no console error in this case, precisely because matching succeeded and only the output has no destination.

Should child routes use relative or absolute paths?

Use relative paths inside a nested tree. With a parent route at /dashboard, a child written as path="settings" composes to /dashboard/settings, whereas an absolute path breaks out of the parent matching relationship. The page shown for the parent path itself is <Route index element={<Overview />} />. To put a login check around a group of pages, wrap them in <Route element={<ProtectedRoute><Outlet /></ProtectedRoute>}> and navigate(-1) after signing in to return to the original page.

Should filter state live in the path or in the query string?

In the query string. Read and write it with const [params, setParams] = useSearchParams(): setParams({ q: "keyword", page: 1 }) replaces the whole query string in one go, params.get("q") reads a single value and params.toString() serialises the lot when you need to build a URL elsewhere. Query state is shareable, bookmarkable and survives a reload. Reserve path params for identity, meaning which entity is in view, and read them with const { id } = useParams(). When a route must swallow several segments, declare <Route path="/files/*" element={<Files />} /> and read the tail from useParams()["*"].

How do I lazy-load just a couple of pages?

Split the component with lazy so it becomes an on-demand chunk, const Page = lazy(() => import("./Page")), and put a Suspense boundary around it in the route that renders it: <Route path="about" element={<Suspense><About /></Suspense>} />. Placing the boundary on the specific route rather than at the app root keeps the fallback scoped: only that route chunk shows a placeholder while the rest of the app stays navigable. Once a route is lazy, the browser no longer restores scroll for you, so pair it with <ScrollRestoration />.

Is a redirect component or an inline auth check the better fit?

Make the check part of the route tree so the protected subtree is never mounted at all: wrap the group in <Route element={<ProtectedRoute><Outlet /></ProtectedRoute>}> and have ProtectedRoute return <Navigate to="/login" replace /> when unauthenticated. replace matters here, otherwise the history keeps an entry for a page the user was never allowed to see and they bounce back to the login screen after signing in. You can also attach state during the redirect so the user lands back where they started.

Official References

Each command links to its official documentation below, so you can verify the latest usage and read deeper.

Maintained by LaoHand

Publicly updated on Jul 21, 2026, continuously proofread against official docs.

Contact Us

Wrong command or description? Send us corrections, business inquiries or product feedback by email.

Contact Us