Search code examples
cssreactjstypescriptreact-router-dom

Sync the current route and state of a component using react router dom


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.


Solution

  • 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>
      );
    };