React Router v6 to v7 Migration: Why Breaking Changes Keep Hurting and How to Survive
The Problem
I upgraded my React app and got hit with this:
ERROR in ./src/App.tsxModule '"react-router-dom"' has no exported member 'BrowserRouter'.Module '"react-router-dom"' has no exported member 'Switch'.Wait, BrowserRouter is gone? After years of using React Router, the library I trusted for routing suddenly broke my app.
Let me check the changelog:
BREAKING CHANGES in v7:- Remix routing patterns are now the default- Component-based routing (BrowserRouter, Switch) deprecated- Data router pattern required- Future flags from v6 are now default behaviorsI was frustrated. Why do they keep making breaking changes?
The Context: Why React Router Keeps Breaking Things
After digging into the history, I found the root cause.
The Remix Merger
In 2022, Shopify acquired Remix. Then the Remix team merged their routing system into React Router v7. This is not just an upgrade - it is a paradigm shift:
v6 (2021) v7 (2024) | | v vLibrary Framework | | v vComponent Data Router | | v vClient-only SSR-readyThe React Router team prioritized framework features over backward compatibility. Forward compatibility was sacrificed for Remix integration.
The Community Pain
I saw this on Reddit:
“Most recently, somewhat begrudgingly because these guys make breaking changes for fun”
“So many times I’ve been stung by painful migrations because they don’t realise forwards compat is a feature”
“React Router pretty much feels like abandonware at this point”
The frustration is real. Each major version requires significant migration effort, documentation lags behind, and simple TypeScript PRs languish unmerged.
How I Migrated: The Future Flags Approach
The key to a smooth migration is incremental adoption using future flags. Do not jump straight to v7.
Step 1: Stay on v6 and Enable Flags
First, ensure you are on the latest v6:
npm install react-router-dom@^6.22.0Then enable future flags one by one:
import { createBrowserRouter } from 'react-router-dom'
const router = createBrowserRouter([ // your routes here], { future: { v7_normalizeFormMethod: true, // Start with this }})Run your tests. Fix any issues. Then add the next flag:
const router = createBrowserRouter(routes, { future: { v7_normalizeFormMethod: true, v7_prependBasename: true, // Add this after tests pass }})Continue this process until all flags are enabled:
const router = createBrowserRouter(routes, { future: { v7_normalizeFormMethod: true, v7_prependBasename: true, v7_relativeSplatPath: true, v7_fetcherPersist: true, v7_partialHydration: true, v7_skipActionErrorRevalidation: true, }})Step 2: Migrate to Data Router Pattern
v6 component-based routing still works but is deprecated. v7 requires the data router pattern.
Before (v6 style):
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
function App() { return ( <BrowserRouter> <nav> <Link to="/">Home</Link> <Link to="/users">Users</Link> </nav> <Routes> <Route path="/" element={<Home />} /> <Route path="/users" element={<Users />} /> <Route path="/users/:id" element={<UserDetail />} /> </Routes> </BrowserRouter> )}After (v7 style):
import { createBrowserRouter, RouterProvider, Link, Outlet } from 'react-router-dom'
const routes = [ { path: "/", element: <Layout />, children: [ { index: true, element: <Home /> }, { path: "users", element: <Users />, children: [ { path: ":id", element: <UserDetail /> } ] } ] }]
const router = createBrowserRouter(routes)
function App() { return <RouterProvider router={router} />}
function Layout() { return ( <> <nav> <Link to="/">Home</Link> <Link to="/users">Users</Link> </nav> <Outlet /> </> )}Step 3: Move Data Fetching to Loaders
The biggest mental shift is moving from useEffect data fetching to loaders.
Before (v6 pattern):
function Users() { const [users, setUsers] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null)
useEffect(() => { fetch('/api/users') .then(r => r.json()) .then(data => { setUsers(data) setLoading(false) }) .catch(err => { setError(err.message) setLoading(false) }) }, [])
if (loading) return <Spinner /> if (error) return <Error message={error} />
return <UserGrid users={users} />}After (v7 pattern):
const routes = [ { path: "/users", element: <Users />, loader: async () => { const res = await fetch('/api/users') if (!res.ok) throw new Error('Failed to load users') return res.json() }, errorElement: <ErrorBoundary /> }]import { useLoaderData, useNavigation } from 'react-router-dom'
function Users() { const users = useLoaderData() // Data already loaded const navigation = useNavigation()
if (navigation.state === "loading") { return <Spinner /> }
return <UserGrid users={users} />}The benefits are immediate:
- Parallel data loading for nested routes
- Built-in error handling
- Automatic caching and revalidation
Step 4: Handle Forms with Actions
Form handling changes dramatically.
Before (v6 pattern):
function CreatePost() { const navigate = useNavigate() const [submitting, setSubmitting] = useState(false)
async function handleSubmit(e) { e.preventDefault() setSubmitting(true)
const formData = new FormData(e.target) try { await createPost(formData) navigate('/posts') } catch (err) { alert(err.message) } finally { setSubmitting(false) } }
return ( <form onSubmit={handleSubmit}> <input name="title" /> <textarea name="content" /> <button disabled={submitting}>Submit</button> </form> )}After (v7 pattern):
import { redirect } from 'react-router-dom'
const routes = [ { path: "/posts/new", element: <CreatePost />, action: async ({ request }) => { const formData = await request.formData() const post = await createPost(formData) return redirect(`/posts/${post.id}`) } }]import { Form, useNavigation } from 'react-router-dom'
function CreatePost() { const navigation = useNavigation() const isSubmitting = navigation.state === "submitting"
return ( <Form method="post"> <input name="title" /> <textarea name="content" /> <button disabled={isSubmitting}> {isSubmitting ? 'Saving...' : 'Submit'} </button> </Form> )}Step 5: Add Error Boundaries
v7 expects explicit error handling:
import { useRouteError, isRouteErrorResponse } from 'react-router-dom'
const routes = [ { path: "/", element: <Layout />, errorElement: <RootErrorBoundary />, children: [ // routes ] }]
function RootErrorBoundary() { const error = useRouteError()
if (isRouteErrorResponse(error)) { return ( <div> <h1>{error.status} {error.statusText}</h1> <p>{error.data}</p> </div> ) }
return <div>Something went wrong</div>}Step 6: Upgrade to v7
After all flags are enabled and tests pass:
npm install react-router-dom@^7.0.0All future flags are now default. Remove them from your config:
// Flags are no longer needed in v7const router = createBrowserRouter(routes)Common Mistakes I Made
Mistake 1: Jumping Straight to v7
I tried to upgrade directly. The app exploded with cryptic errors.
Wrong:
npm install react-router-dom@latest# 500+ type errors, broken routingRight:
# Stay on v6, enable flags firstnpm install react-router-dom@^6.22.0# Enable all flags, test, then upgradenpm install react-router-dom@^7.0.0Mistake 2: Mixing Router Patterns
I tried to nest old and new routers together.
Wrong:
function App() { return ( <RouterProvider router={router}> <BrowserRouter> {/* Do not do this */} <Routes>...</Routes> </BrowserRouter> </RouterProvider> )}Right:
function App() { return <RouterProvider router={router} />}Mistake 3: Forgetting Pending States
The first time users navigate, data needs to load. I forgot loading states.
Wrong:
function Users() { const users = useLoaderData() return <UserGrid users={users} /> // Crashes if data not ready}Right:
function Users() { const users = useLoaderData() const navigation = useNavigation()
if (navigation.state === "loading") { return <Spinner /> }
return <UserGrid users={users} />}Mistake 4: Not Using defer for Slow Data
Some data loads slowly. Use defer for progressive loading:
import { defer } from 'react-router-dom'
const routes = [ { path: "/dashboard", element: <Dashboard />, loader: async () => { // Fast data first const user = await fetchUser()
// Slow data deferred return defer({ user, analytics: fetchAnalytics() // Promise, not awaited }) } }]import { useLoaderData, Await } from 'react-router-dom'import { Suspense } from 'react'
function Dashboard() { const { user, analytics } = useLoaderData()
return ( <div> <h1>Welcome, {user.name}</h1>
<Suspense fallback={<Spinner />}> <Await resolve={analytics}> {(data) => <AnalyticsChart data={data} />} </Await> </Suspense> </div> )}Migration Timeline
For a medium-sized project (20-30 routes), I budgeted:
Week 1: Enable future flags (2-3 days) - Day 1-2: Enable v7_normalizeFormMethod, v7_prependBasename - Day 3: Enable remaining flags, fix issues
Week 2: Convert to data router pattern (3-4 days) - Day 1: Restructure routes - Day 2-3: Convert loaders/actions - Day 4: Error boundaries
Week 3: Testing and cleanup (2-3 days) - Day 1-2: E2E testing - Day 3: Upgrade to v7, final testsShould You Migrate?
Consider the alternatives:
| Option | Pros | Cons |
|---|---|---|
| Stay on v6 | No migration cost | Eventually unsupported |
| Migrate to v7 | Long-term support, new features | Migration effort |
| TanStack Router | Modern, better DX | Another migration |
If your project is small or you are starting fresh, TanStack Router is worth considering. For existing v6 projects, the future flags approach makes migration manageable.
Summary
React Router’s breaking changes stem from the Remix merger. The team chose framework features over backward compatibility. But with future flags, you can migrate incrementally:
- Stay on v6.22+
- Enable future flags one by one
- Convert to data router pattern
- Move data fetching to loaders
- Handle forms with actions
- Add error boundaries
- Upgrade to v7
The migration is painful, but the result is a more powerful routing system with built-in data loading, error handling, and SSR support.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 React Router v7 Documentation
- 👨💻 React Router v7 Migration Guide
- 👨💻 React Router Future Flags Reference
- 👨💻 TanStack Router (Alternative)
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments