Is there a way to sync the active route in the browser with the state of a component?
When I click on "Route 2" in the navbar, the element is highlighted because it is the active route, and the route in the browser shows "/route2"
. If I refresh the browser, "Route 1" is now highlighted because it is initial state of the Navbar
component, but the route in the browser still shows "/route2"
. Is there a way to sync the two?
To replicate the project, create a new vite
project with react-ts
, then add the following files:
App.tsx
import React from "react";
import { Routes, Route, Navigate, Link, Outlet } from "react-router-dom";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
const Navbar = () => {
const [selected, setSelected] = React.useState(0);
return (
<div className="flex justify-between items-center sticky top-0 border-b bg-background px-4 h-12 z-10">
<h1>App Name</h1>
<div className="flex gap-4 h-8 items-center">
<Link to="/route1" onClick={() => setSelected(0)}>
<h2
className={cn(
"hover:underline hover:text-blue-500",
selected === 0 ? "text-blue-500" : null
)}
>
Route 1
</h2>
</Link>
<Link to="route2" onClick={() => setSelected(1)}>
<h2
className={cn(
"hover:underline hover:text-blue-500",
selected === 1 ? "text-blue-500" : null
)}
>
Route 2
</h2>
</Link>
</div>
</div>
);
};
const Layout = () => {
return (
<div>
<Navbar />
<div className="px-2 py-4">
<Outlet />
</div>
</div>
);
};
const CompA = () => {
return <div>Component A</div>;
};
const CompB = () => {
return <div>Component B</div>;
};
const Route1 = () => {
return (
<div className="flex flex-col gap-4">
<div className="flex justify-center">
<Link to="/route1/a">
<h2 className="border p-2 hover:bg-gray-200">Component A</h2>
</Link>
<Link to="/route1/b">
<h2 className="border p-2 hover:bg-gray-200">Component B</h2>
</Link>
</div>
<div className="border">
<Outlet />
</div>
</div>
);
};
const Route2 = () => {
return <div>Route2</div>;
};
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Navigate to="/route1" replace />} />
<Route path="route1" element={<Route1 />}>
<Route index element={<Navigate to="/route1/a" replace />} />
<Route path="a" element={<CompA />} />
<Route path="b" element={<CompB />} />
</Route>
<Route path="route2" element={<Route2 />} />
</Route>
</Routes>
);
}
export default App;
package.json
{
"name": "nested-routes",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"clsx": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.18.0",
"tailwind-merge": "^2.0.0"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.16",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.31",
"prettier": "^3.0.3",
"tailwindcss": "^3.3.5",
"typescript": "^5.0.2",
"vite": "^4.4.5"
}
}
tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ["index.html", "./src/**/*.tsx"],
theme: {
extend: {},
},
plugins: [],
}
postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import { BrowserRouter } from 'react-router-dom'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
)
index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Lastly, remove contents of App.css.
Instead of using the base Link
component and manually computing the active route/link, use the NavLink
component that handles this automatically. The NavLink
's children
prop can take a render function prop value that is passed an isActive
prop to be used to conditionally apply UI logic in the wrapped children UI.
<NavLink to="/tasks"> {({ isActive, isPending, isTransitioning }) => ( <span className={isActive ? "active" : ""}>Tasks</span> )} </NavLink>
Example:
...
import {
Routes,
Route,
Navigate,
NavLink,
Outlet
} from "react-router-dom";
...
const Navbar = () => {
return (
<div className="flex justify-between items-center sticky top-0 border-b bg-background px-4 h-12 z-10">
<h1>App Name</h1>
<div className="flex gap-4 h-8 items-center">
<NavLink to="/route1">
{({ isActive }) => (
<h2
className={cn(
"hover:underline hover:text-blue-500",
isActive ? "text-blue-500" : null
)}
>
Route 1
</h2>
)}
</NavLink>
<NavLink to="route2">
{({ isActive }) => (
<h2
className={cn(
"hover:underline hover:text-blue-500",
isActive ? "text-blue-500" : null
)}
>
Route 2
</h2>
)}
</NavLink>
</div>
</div>
);
};