# Redirect Based on Screen Size in React Router

Used: react-router@7.0.0

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`).

```ts {% path="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>`.

```tsx {% path="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.

```tsx {% path="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.

```tsx {% path="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.
