Routing with less code in React

2025-09 Alexander Tkačenko

The React routers I came across left me wondering: if route-based rendering is essentially conditional rendering, can we actually bring this idea into practice? Can we have the conveniences of responsiveness to URL changes, static and dynamic URL matching with something similar in shape to the ordinary conditional rendering, like:

{atRoute ? x : y}
{atRoute && x}

It would work the same way with components and prop values, and it would make the routing code more concise and semantically clearer compared to what routers tend to offer. After playing around with this idea, I still believe it's a way to go.

Below is an example of what it may look like. Here, we have a hook that subscribes the React component to URL changes and returns a ternary routing function at(route, x, y) acting as a close semantic equivalent to the ternary conditional operator atRoute ? x : y.

const App = () => {
  const { at } = useRoute();

  return (
    <>
      <nav className={at("/", "full", "compact")}>
        <A href="/">Intro</A>{" "}
        <A href="/sections/1">Section 1</A>{" "}
        <A href="/sections/2">Section 2</A>
      </nav>
      {at("/", <Intro/>)}
      {at(/^\/sections\/(?<id>\d+)\/?$/, ({ params }) => (
        <Section id={params.id}/>
      ))}
    </>
  );
};

createRoot(document.querySelector("#app")).render(<App/>);

The at(route, x, y) function allows to omit the fallback parameter y when it's effectively undefined (as in at("/", <Intro/>) in the example above) and to pass URL pattern parameters to dynamic values (like with ({ params }) => <Section id={params.id}/>).

Even without going into detail, this code seems pretty easy to follow. And to write, too.

As a quick comparison, it's worth noting how much more code is required to get a very similar output with TanStack Router (and how more elaborate this code is).

View the TanStack Router code example from the TanStack docs
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import {
  Outlet,
  RouterProvider,
  Link,
  createRouter,
  createRoute,
  createRootRoute,
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'

const rootRoute = createRootRoute({
  component: () => (
    <>
      <div className="p-2 flex gap-2">
        <Link to="/" className="[&.active]:font-bold">
          Home
        </Link>{' '}
        <Link to="/about" className="[&.active]:font-bold">
          About
        </Link>
      </div>
      <hr />
      <Outlet />
      <TanStackRouterDevtools />
    </>
  ),
})

const indexRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/',
  component: function Index() {
    return (
      <div className="p-2">
        <h3>Welcome Home!</h3>
      </div>
    )
  },
})

const aboutRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/about',
  component: function About() {
    return <div className="p-2">Hello from About!</div>
  },
})

const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])

const router = createRouter({ routeTree })

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

const rootElement = document.getElementById('app')!
if (!rootElement.innerHTML) {
  const root = ReactDOM.createRoot(rootElement)
  root.render(
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>,
  )
}

As seen from our first code snippet, the route-matching function at(route, x, y) works the same way with both components and prop values (like with <Intro/> and the <nav>'s className). By contrast, the file-based, component-based, and config-based approaches require different patterns for that.

Since at(route, x, y) from the first example is an ordinary stateless function, at() calls aren't coupled together, they don't have to maintain a certain order, they don't have to be grouped in a single component (and they also can be, as in our small example), pretty much like any other conditional rendering. This makes the routing function at() very flexible and fit for arbitrary route-based logic (more so than other approaches to routing).

I integrated this approach into an NPM package of mine, react-statepack. Its minimalism and straightforwardness are ultimately the result of bringing the routing code closer to common patterns and making it more intuitive regarding common aspects of routing, like route matching, the route link and navigation APIs, SSR, lazy routes, URL parameters state, type-safe routes, view transitions. These topics are covered in more detail in the package description.