How toRedirect Based on Screen Size in React Router
When building responsive layouts, you may want certain routes to behave differently depending on the viewport. For example, imagine you have a /settings
page:
- On mobile:
/settings
shows a list of navigation links to individual setting pages. - On desktop:
/settings
should immediately redirect to the first settings page because showing only the sidebar is poor UX.
We could check the viewport in the layout’s clientLoader
, but that means parsing the URL manually and redirecting regardless of the matched route. A cleaner approach is to create a dedicated index route that matches only /settings
and decides whether to render or redirect based on screen size. This lets us:
- Run the redirect before render to avoid a jarring flash.
- Avoid regex hacks by letting routing match exactly when we want.
- Stay responsive to viewport changes by listening for media query changes.
Defining the Routes
We start by defining the route tree so that /settings
has an index route (settings._index.tsx
) and a parameterized detail route (settings.$id.tsx
).
app/routes.ts import { type RouteConfig, index, route } from "@react-router/dev/routes"; export default [ route("settings", "routes/settings.tsx", [ index("routes/settings._index.tsx"), route(":id", "routes/settings.$id.tsx"), ]), ] satisfies RouteConfig;
This structure ensures that the index route matches only /settings
, while /settings/:id
matches the detail view.
The Layout Component
The layout shows the settings navigation and renders the active child route in an <Outlet>
.
app/routes/settings.tsx import { Outlet, Link, href } from "react-router"; import type { Route } from "./+types/settings"; export function loader() { return { options: [ { to: href("/settings/:id", { id: "1" }), label: "Settings 1" }, { to: href("/settings/:id", { id: "2" }), label: "Settings 2" }, { to: href("/settings/:id", { id: "3" }), label: "Settings 3" }, { to: href("/settings/:id", { id: "4" }), label: "Settings 4" }, ], }; } export default function Component({ loaderData }: Route.ComponentProps) { return ( <div> <ul> {loaderData.options.map((option) => ( <li key={option.to}> <Link to={option.to}>{option.label}</Link> </li> ))} </ul> <hr /> <Outlet /> </div> ); }
On mobile, this list is shown when visiting /settings
. On desktop, we’ll redirect away from this view entirely.
The Detail Route
The detail route simply displays the setting ID.
app/routes/settings.$id.tsx import type { Route } from "./+types/settings.$id"; export default function Component({ params }: Route.ComponentProps) { return <h1>Setting {params.id}</h1>; }
This is where desktop users will land by default when they try to visit /settings
.
The Index Route with Redirect Logic
This is the core of the solution. The clientLoader
runs before rendering the page:
- If the screen is mobile-sized (
max-width: 720px
), it returns amediaQuery
object so we can keep listening for changes. - If desktop, it immediately redirects to
/settings/1
.
The component also listens for viewport changes — if the user switches from mobile to desktop, it navigates away.
app/routes/settings._index.tsx import { useLayoutEffect } from "react"; import { href, redirect, useNavigate } from "react-router"; import type { Route } from "./+types/settings._index"; export async function clientLoader() { let mediaQuery = window.matchMedia("(max-width: 720px)"); if (mediaQuery.matches) return { mediaQuery }; return redirect(href("/settings/:id", { id: "1" })); } export default function Component({ loaderData }: Route.ComponentProps) { let navigate = useNavigate(); useLayoutEffect(() => { loaderData.mediaQuery.addEventListener("change", listener); return () => loaderData.mediaQuery.removeEventListener("change", listener); function listener(event: MediaQueryListEvent) { if (event.matches) return; navigate(href("/settings/:id", { id: "1" })); } }, [navigate, loaderData.mediaQuery]); return null; }
This way, the redirect happens before any rendering on desktop, preventing flashes, and the experience adapts live if the user resizes their browser.