Fix: A Route is only ever to be used as the child of Routes
Why v5-style routing code crashes on React Router 6 and 7, the two-step migration that fixes it, and the second error that appears if you only do half of it.
TL;DR#
React Router 6 changed how routes are declared. A <Route> is no longer a component that renders itself: it is a configuration element that only a parent <Routes> reads. Rendering one anywhere else makes its component throw immediately. The fix has two parts, and you need both: wrap every group of routes in <Routes>, and move each page from the children of <Route> to its element prop, as in <Route path="/welcome" element={<Welcome />} />.
Error: A <Route> is only ever to be used as the child of <Routes> element, never rendered directly. Please wrap your <Route> in a <Routes>.The error comes from a Stack Overflow question asked on 3 November 2021, viewed more than 230,000 times. The asker was following a Udemy course, and the second-highest answer points out that the course appears to use React Router 5. The npm registry shows that react-router-dom 6.0.0 was published on the same date, 3 November 2021: code written for 5 and a library installed as 6 is exactly the mismatch this error reports.
The code that triggers it#
This is the shape of the question's App.js: three routes rendered straight inside a <div>, each page passed as a child, with a BrowserRouter around <App /> in index.js.
import { Route } from "react-router-dom";
import Welcome from "./Pages/Welcome";
import Game from "./Pages/Game";
import Leaderboard from "./Pages/Leaderboard";
function App() {
return (
<div>
<Route path="/welcome">
<Welcome />
</Route>
<Route path="/game">
<Game />
</Route>
<Route path="/leaderboard">
<Leaderboard />
</Route>
</div>
);
}
export default App;In React Router 5 this was valid code. Each <Route> read the current location from the router context and rendered its children when its path matched. In React Router 6 and 7 it crashes on the first render.
Why it happens#
The React Router source makes the mechanism plain. The Route component has no rendering logic at all: its whole body is an invariant(false, ...) call that throws the message above. A <Route> is never meant to be rendered. <Routes> walks its children, reads their props (path, element, nested routes) into a route configuration, picks the best match for the URL, and renders that match's element. A <Route> that is rendered by anything other than <Routes> reaches its own function body, and the function body is a throw.
The official v5 to v6 upgrade guide states both requirements. Under its heading on upgrading every <Switch> element to <Routes>, it says an app must convert all of its <Switch> elements to <Routes> to use v6. A few paragraphs later it notes that every <Route children> from the v5 app becomes <Route element> in v6, which for code already on v5.1 means moving the route element from the child position to the named element prop. And it gives the reason children cannot keep their old meaning: in v6, <Route children> is reserved for nesting routes.
That reservation is why fixing only the wrapper is not enough, as the next section shows.
Fix: migrate the route block#
1. Import Routes and wrap the routes in it#
import { Routes, Route } from "react-router-dom";<Routes> replaces v5's <Switch>. If your code had a <Switch>, rename it; if it had bare routes like the question's, add one around them.
2. Move each page to the element prop#
import { Routes, Route } from "react-router-dom";
import Welcome from "./Pages/Welcome";
import Game from "./Pages/Game";
import Leaderboard from "./Pages/Leaderboard";
function App() {
return (
<div>
<Routes>
<Route path="/welcome" element={<Welcome />} />
<Route path="/game" element={<Game />} />
<Route path="/leaderboard" element={<Leaderboard />} />
</Routes>
</div>
);
}
export default App;Keep BrowserRouter in index.js exactly where it was. <Routes> needs a router above it; the router is not what was missing.
3. Remove props that v6 no longer reads#
If the v5 code used exact, delete it. The upgrade guide says "<Route exact> is gone": routes now match by best fit rather than first match, and a route whose component declares further descendant routes uses a trailing * in its path (path="users/*") to keep matching deeper URLs. The component={Page} and render={...} props of v5 are replaced the same way as children, by element={<Page />}.
Verify the fix#
The full sequence was reproduced on 13 October 2026 in a throwaway node:22-bookworm-slim container: the React.createElement equivalent of the code above, rendered with renderToString from react-dom/server inside a MemoryRouter at /welcome. With react-router-dom 6.30.6 and react 18.3.1, the question's code fails with the exact error:
== react-router-dom 6.30.6, react 18.3.1, node v22.23.3
$ node render.mjs broken
Error: A <Route> is only ever to be used as the child of <Routes> element, never rendered directly. Please wrap your <Route> in a <Routes>.After both steps, the matching page renders:
$ node render.mjs fixed
<div><h1>Welcome page</h1></div>The current major behaves the same. With react-router-dom 7.18.4, the unchanged code throws the identical message, and the migrated code renders the welcome page. Nothing about this error changed between 6 and 7, so the fix above applies to both. In a browser, check the same thing directly: open /welcome, then /game, and confirm each URL shows only its own page.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
The second error: wrapping without moving the page#
The most common half-fix is to add <Routes> and leave the pages as children. The first error disappears and a new one replaces it, naming your page component:
$ node render.mjs children
Error: [Welcome] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>This is the same rule seen from the other side. Because <Route children> is reserved for nested routes, <Routes> reads <Welcome /> as a nested route definition, checks that it is a <Route>, and throws because it is not. The bracketed name is the component's function name, so the message tells you which route still has its page in the children position. Move that page to element and the error goes away. The same check runs in react-router-dom 7.18.4 and printed the same line in the reproduction.
Guard components that wrap a Route#
v5 apps often protected pages with a PrivateRoute component that returned a <Route> or a redirect. Placed inside <Routes>, that pattern fails the same check, because <Routes> compares each child's type to Route itself, not to whatever the child would render:
$ node extra.mjs wrapper
Error: [PrivateRoute] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>The v6 shape puts the guard inside element, so the child of <Routes> is a real <Route> again:
import { Navigate } from "react-router-dom";
function RequireAuth({ user, children }) {
return user ? children : <Navigate to="/login" replace />;
}
// inside <Routes>
<Route
path="/leaderboard"
element={
<RequireAuth user={currentUser}>
<Leaderboard />
</RequireAuth>
}
/>The upgrade guide documents <Navigate> as the replacement for v5's <Redirect>, and notes one behaviour change worth knowing when you port a guard: <Redirect> replaced the history entry by default, while <Navigate> pushes unless you pass replace. That is why the example passes it.
Grouping routes is still allowed. The error message itself lists <React.Fragment> as a valid child, and the reproduction confirmed that routes wrapped in a fragment inside <Routes> match and render normally.
Downgrading to React Router 5#
One answer on the question suggests pinning the old major instead. That works: with react-router-dom 5.3.0, the question's code, unchanged, rendered the welcome page in the reproduction, on react 17.0.2 and again on react 18.3.1:
== react-router-dom 5.3.0, react 18.3.1, node v22.23.3
$ node v5.cjs
<div><h1>Welcome page</h1></div>It is a way to finish a v5 course, not a fix for an app. Tutorials, examples and new packages target the current API, and every future upgrade will meet this same migration. For a small route block like this one, the two steps above take less time than the downgrade.
What tends to break next#
Once the routes render, the next problems are usually not about routing syntax. A route change unmounts the page you leave, so a page that starts a fetch and calls setState when it resolves is updating a component that no longer exists. On React 17 and earlier, which React Router 6 still accepts, that logs the state update on an unmounted component warning, and the fix there is cancelling the request in the effect cleanup. React 18 removed that warning, according to the 18.0.0 changelog, but cancelling the request is still the clean pattern. If a page's effects appear to run twice in development, that is React 18 Strict Mode, not the router: see why useEffect runs twice. And if this project is moving to Next.js, the router-context errors look different there: a component calling useRouter() outside a Pages Router tree throws NextRouter was not mounted.
The error message is literal once the v6 model is clear. A <Route> is a description of a route, read by <Routes>, never a component that draws a page. Put every route inside a <Routes>, give each one its page through element, and keep children for routes nested inside other routes.
Further reading: Access to fetch at accounts.google.com blocked by CORS: fix.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Next.js: Error: only absolute urls are supported (fix)
Why fetch("/api/products") works after a client-side click but throws on refresh, what each fetch implementation prints, and the two fixes that hold in both routers.
Fix Error: NEXT_REDIRECT logged in a Route Handler catch
Reproduced on Next.js 16.4.2: why redirect() inside try/catch logs Error: NEXT_REDIRECT, sends users to the wrong URL or returns a 500, and the three fixes that hold.
How to Access Route Parameter Inside getServerSideProps
Learn why your dynamic route parameter appears as undefined in getServerSideProps and how to correctly extract it from the context object for server-side data fetching.


