-
-
Notifications
You must be signed in to change notification settings - Fork 10.8k
Description
I'm using React Router as a...
framework
Reproduction
https://stackblitz.com/edit/github-7pxkksy3?file=app%2Froot.tsx
Project setup
root.tsx
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
NavLink,
} from 'react-router';
import './app.css';
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<nav>
<ul>
<li>
<NavLink to="/" end>
Home
</NavLink>
</li>
<li>
<NavLink to="/other">Other</NavLink>
</li>
</ul>
</nav>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}routes.ts
import { type RouteConfig, index, route } from '@react-router/dev/routes';
export default [
index('routes/home.tsx'),
route('other', 'routes/other.tsx'),
] satisfies RouteConfig;app.css
nav a.active {
background-color: #e76829;
}
routes/home.tsx
export default function Home() {
return (
<div className="text-center p-4">
<h1 className="text-2xl">Hello, Home</h1>
</div>
);
}routes/other.tsx
export default function Other() {
return (
<div className="text-center p-4">
<h1 className="text-2xl">Hello, Other</h1>
</div>
);
}react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
} satisfies Config;Steps to reproduce
- run
react-router buildin the terminal - serve
build/clientwith a webserver
cd ./build/clientminiserve --spa --index index.html(using https://github.com/svenstaro/miniserve as an example but can use whatever server for SPA)
- open
localhost:8080,NavLinkfor "home" should be active (orange background) - click on
NavLinkfor "other", this navigates and makes it active - refresh the page now the
NavLinkfor "home" is active again even though we're looking at the "other" page
- this also happens when just skipping step 3 and 4 and navigating to
localhost:8080/otherdirectly
System Info
System:
OS: Windows 11 10.0.22631
CPU: (8) x64 11th Gen Intel(R) Core(TM) i5-1145G7 @ 2.60GHz
Memory: 1.17 GB / 15.39 GB
Binaries:
Node: 22.13.1 - ~\scoop\apps\nvm\current\nodejs\nodejs\node.EXE
npm: 10.9.2 - ~\scoop\apps\nvm\current\nodejs\nodejs\npm.CMD
Browsers:
Edge: Chromium (131.0.2903.146)
Internet Explorer: 11.0.22621.3527
npmPackages:
@react-router/dev: ^7.1.3 => 7.1.5
@react-router/node: ^7.1.3 => 7.1.5
react-router: ^7.1.3 => 7.1.5
vite: ^6.0.7 => 6.1.0Used Package Manager
npm
Expected Behavior
When serving the build/client directory with a server and directly visiting localhost:8080/other the NavLink for "other" should be active.
Actual Behavior
When directly visiting localhost:8080/other the NavLink for "home" is active.
Note that if you have multiple NavLink "home" stays active until you visit the home route and navigate away again. Navigating to any other route while "home" is active will show two active NavLinks.
Workaround
Moving the whole <nav> into the routes/home.tsx and routes/other.tsx directly seems to fix the issue, when reloading the page the correct NavLink becomes active.
Potential cause
When looking at build/client/index.html we can see that the NavLinks have been pre-rendered (as described in the docs https://reactrouter.com/how-to/spa#important-note) and the "home" NavLink has been pre-rendered with class="active".
Somehow after hydration this active class should switch to the correct NavLink but it doesn't. Interestingly there are no hydration warnings from React in the console.
Potential solutions
This could very well be me "misusing" NavLinks (that they are not supposed to be rendered in <Layout />), in which case I hope I can be pointed to the right docs or that I can help update the documentation to make this behaviour clearer.
Otherwise I see two strategies:
NavLinks should pre-render without their active class- After hydration the
NavLinksshould rerender to show the active class