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:

  1. Run the redirect before render to avoid a jarring flash.
  2. Avoid regex hacks by letting routing match exactly when we want.
  3. 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 a mediaQuery 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.