React Router v7 comes with a big shift. It introduces three new modes, a full Remix merge, and support for file-based routing. But, unlike many other frameworks, it’s not forced on you. And that’s interesting. Since a lot of people love file-based routing, it should at least be the default, right? Wrong.
In this post, we’ll explore React Router’s evolution, how Remix influenced the new mode that introduced file-based routing in v7, why it remains optional, and when it makes sense to use it or stick with a different approach.
React Router was first released in November 2014 by Michael Jackson (not that M.J.) and Ryan Florence. At the time, most React developers were still figuring out how to handle navigation in single-page apps without resorting to low-level history manipulation. React Router provided a simple alternative. It lets you declare routes as JSX elements, which fit naturally into React’s component model and way of thinking.
Its early appeal was simple:
Over the years, the library evolved and added features like nested routing, dynamic parameters, and lazy loading before most other React frameworks caught up. In 2021, v6 introduced an API redesign that improved route nesting, reduced bundle size, and improved type safety. Meanwhile, Michael and Ryan were also working on a new project called Remix that would push routing concepts even further. That parallel effort set the stage for React Router’s biggest shift.
Remix began life as a full-stack web framework built on top of React Router. While React Router focused on the client-side navigation layer, Remix added file-based routing, Server-Side Rendering (SSR) out of the box, data loading and mutation APIs tied directly to routes, and many other features. If React Router were the engine, Remix would be a complete car built around it.
Over time, the gap between the two projects narrowed. Much of Remix’s routing code was React Router. Many React Router users wanted SSR and data APIs, while many Remix users appreciated the low-level control of React Router. That overlap led to a clear conclusion of folding Remix’s routing and data layer directly into React Router.
To avoid disrupting the existing patterns that developers are already familiar with, the team decided to split the library into three modes: declarative mode and data mode, both of which remain in the React Router syntax you’re already familiar with, and framework mode, which brings a Remix-like pattern to React Router.
The three new modes are structured so you can pick the level of abstraction you need, without being forced into conventions you don’t want. Let’s see their differences below.
Declarative mode is the lightest way to use React Router. It focuses on the basic functionalities like matching URLs to components and navigating between pages. It gives you APIs such as <Link>
, useNavigate
, and useLocation
for client-side navigation and stays out of the way when it comes to data loading or server rendering.
To get started, you can create a new React app and install React Router:
npx create-vite@latest npm i react-router
Once that is set up, wrap your app in a <BrowserRouter>
and define your routes with <Routes>
and <Route>
:
import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter, Routes, Route } from "react-router"; import Home from "./pages/Home"; import About from "./pages/About"; import Settings from "./pages/Settings"; ReactDOM.createRoot(document.getElementById("root")).render( <BrowserRouter> <Routes> <Route path="/" element={<Home />} /> <Route path="about" element={<About />} /> <Route path="settings" element={<Settings />} /> </Route> </Routes> </BrowserRouter> );
As you might have noticed, this is still the previous React Router syntax you’re familiar with, but in a new mode. This mode is encouraged when you want a small—to medium-sized SPA or prefer a routing layer that stays lean and predictable.
Data mode moves your route configuration out of React’s render tree and gives you built-in support for data loading, data mutations, and pending states. It builds on existing concepts like loaders for fetching data before a route renders, actions for handling mutations, and helper APIs such as useLoaderData
, useActionData
, and useFetcher
to work with that data inside components.
To start using data mode, you create your routes with the createBrowserRouter()
method instead of JSX <BrowserRouter>
. That way, you can define each route as an object with properties for its path, component, and any data functions it needs:
import React from "react"; import ReactDOM from "react-dom/client"; import { createBrowserRouter, RouterProvider, useLoaderData } from "react-router"; function Home() { const data = useLoaderData(); return <h1>Welcome {data.user}</h1>; } const router = createBrowserRouter([ { path: "/", Component: Home, loader: async () => { return { user: "Alice" }; } }, { path: "/about", Component: () => <h1>About</h1> } ]); ReactDOM.createRoot(document.getElementById("root")).render( <RouterProvider router={router} /> );
In this example, the loader runs before the <Home>
component renders, ensuring the data is ready when the component mounts. This makes it easier to coordinate navigation with data.
Just like the declarative mode, the data mode also uses existing React Router patterns. But it’s now encouraged as a good fit when you want routing and data loading to work together, or you need each route to manage its own data lifecycle.
Framework mode is the major change that introduces effortless file-based routing to React Router. It incorporates many features previously found in the Remix framework under a more integrated and opinionated development experience. You can define routes either in a configuration file or by following file naming conventions, and React Router takes care of the rest.
To get started, you can create a new framework mode app by running the following command:
npx create-react-router@latest my-app
And you should have a Remix-styled React Router application already set up. But by default, you still have to match files to routes, as shown below:
// app/routes.ts import { index, route } from "@react-router/dev/routes"; export default [ index("./home.tsx"), route("products", "./product.tsx"), ];
It’s also worth mentioning that you can export a loader to fetch data or an action to send data in your page files. Everything is automatically type-checked and code-split, and can run with SSR or full static generation:
// products.tsx const getProduct = async () => { return fetch(`https://fakestoreapi.com/products/`).then((res) => res.json()); }; export async function loader({}) { let product = await getProduct(); return { product }; } export default function Product({ loaderData }) { return ( <div> {loaderData.product.map((product) => ( <div key={product.id}>{product.title}</div> ))} </div> ); }
Now, to use the file-based routing feature, you need to first install the @react-router/fs-routes package:
npm i @react-router/fs-routes
Then update your app route configuration to use the flat routes configuration below:
// app/routes.ts import { type RouteConfig } from "@react-router/dev/routes"; import { flatRoutes } from "@react-router/fs-routes"; export default flatRoutes({ ignoredRouteFiles: ["about.tsx"], }) satisfies RouteConfig;
With this update, any modules in the app/routes directory will automatically become routes in your application. Now, you might be wondering why the React Router team didn’t make file-based routing the default. Let’s explore that below.
File-based routing is popular because it feels effortless. You drop a file into a folder and name it after the URL path, and the framework wires it up automatically. For many projects, this speeds up setup and keeps routes organized in a way that mirrors your file structure.
However, there are a couple of edge cases you could run into while using file-based routing.
File-based routing can break down when routes depend on conditions. For example, say you want certain pages to only load in development, or you want to show different routes (not components) based on user roles or feature flags. With file-based routing, there’s no clean way to express that logic in the file system, so you end up patching around the convention or moving back to code-defined routes anyway.
File-based routing works by turning your folders and files into routes. But not every route fits neatly into a file name. For example, if you need something like /user-:id([0-9]+)
or routes that come from a CMS or a database, you can’t really represent those with normal files. In those cases, file-based routing forces you to use weird file names or extra workarounds.
For small applications, the file tree in a file-based routing setup gives you a clear picture of all the routes. However, as the app grows, the folder structure can quickly become hard to manage. Think of a large dashboard with dozens of nested pages or a multilingual site where every language has its own folder of routes. The tree can get deep and overwhelming fast. And because there’s no single config file, it becomes harder to step back and see the full routing setup in one place.
In a recent PodRocket interview, Brooks Lybrand, developer relations manager at React Router, also expressed these sentiments. He explained that no matter how you design it, someone will eventually run into a case where file-based routing feels clunky, so having a lower-level config option is essential. That’s why React Router v7 avoids this trap by making file-based routing optional. You can stick with it for convenience or define routes in code when you need more control. However, that doesn’t mean file-based routing has no place.
Outside the edge cases above, file-based routing provides a smoother development experience than manually configuring routes in code.
You’ll find it useful when:
/settings/profile
or /settings/billing
, which are easy to mirror in foldersFile-based routing is a great option when your project’s structure already fits naturally into a folder tree. It gives you a routing setup that feels almost invisible, so you can focus on building pages and features, while the framework wires everything together in the background.
In this article, we explored the new updates in React Router version 7, how it merged with Remix to add support for file-based routing, how to get started with using it, and most importantly, why it’s not forced on you. React Router v7 strikes a balance between convention and flexibility. You get the convenience of file-based routing if your project fits it and the control of code-defined routes when it doesn’t.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how MCP will replace the traditional browser, what this shift means for frontend devs, and how to start prepping for an AI-first future.
TanStack Start’s Selective SSR lets you control route rendering with server, client, or data-only modes. Learn how it works with a real app example.
Learn how Cursor project rules streamline Next.js apps with automated conventions, consistent components, and faster developer onboarding.
Explore the hidden dangers of BaaS, and how frontend-focused teams can use BaaS platforms without suffering from their major risks.