Heads‑up: In browser apps, import from
react-router-dom
, notreact-router
.
Example:import { createBrowserRouter, RouterProvider, Link, redirect } from "react-router-dom";
This guide shows how to navigate to another route when a button is clicked using React Router v6.4+ with the Data APIs. We’ll cover the production‑ready patterns you actually want in a real app: useNavigate
, link‑equivalents, loader/action redirects, passing state, relative navigation, and common pitfalls.
TL;DR
- Use
useNavigate()
inside a component to navigate on click. - Prefer
redirect()
in loaders/actions for pre‑render redirects (no flicker). - Keep imports from
react-router-dom
in web apps. -
replace: true
prevents polluting history (great for auth flows).
1) Minimal Working Example (TypeScript)
main.tsx
import React from "react"; import ReactDOM from "react-dom/client"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { AppLayout } from "./routes/AppLayout"; import { Home } from "./routes/Home"; import { Dashboard } from "./routes/Dashboard"; import { NotFound } from "./routes/NotFound"; const router = createBrowserRouter([ { path: "/", element: <AppLayout />, children: [ { index: true, element: <Home /> }, { path: "dashboard", element: <Dashboard /> }, { path: "*", element: <NotFound /> }, ], }, ]); ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> <RouterProvider router={router} /> </React.StrictMode> );
routes/AppLayout.tsx
import { Outlet, Link } from "react-router-dom"; export function AppLayout() { return ( <div style={{ padding: 24, fontFamily: "ui-sans-serif, system-ui" }}> <header style={{ display: "flex", gap: 12, marginBottom: 16 }}> <Link to="/">Home</Link> <Link to="/dashboard">Dashboard</Link> </header> <Outlet /> </div> ); }
routes/Home.tsx — navigate on button click
import { useNavigate } from "react-router-dom"; export function Home() { const navigate = useNavigate(); const goToDashboard = () => { // push navigation (back button returns here) navigate("/dashboard", { state: { from: "home", flash: "Welcome to your dashboard!" }, }); }; return ( <section> <h1>Home</h1> <p>Click the button to navigate programmatically.</p> <button onClick={goToDashboard}>Go to Dashboard</button> </section> ); }
routes/Dashboard.tsx — read navigation state
import { useLocation, useNavigate } from "react-router-dom"; export function Dashboard() { const location = useLocation(); const navigate = useNavigate(); const flash = (location.state as any)?.flash as string | undefined; return ( <section> <h1>Dashboard</h1> {flash && <p style={{ color: "rebeccapurple" }}>{flash}</p>} <button onClick={() => navigate(-1)}>⬅ Back</button> <button onClick={() => navigate("/", { replace: true })}> Go Home (replace) </button> </section> ); }
routes/NotFound.tsx
export function NotFound() { return <h1>404 — Not Found</h1>; }
2) Button vs <Link>
vs Form Actions
A) Button + useNavigate
(event‑driven)
Use when clicking a button should navigate after some logic (e.g., permission checks, analytics, async save).
const navigate = useNavigate(); <button onClick={() => { // business logic … navigate("/reports/2025?tab=summary"); }}> View 2025 Summary </button>
B) Anchor‑like navigation (<Link>
)
If you just need a navigation element with no extra logic, prefer <Link>
for a11y + prefetch behavior.
import { Link } from "react-router-dom"; <Link to="/settings?section=profile">Profile Settings</Link>
C) Pre‑render redirect with actions/loaders (no UI flicker)
For auth and post‑submit redirects, prefer the Data API:
// routes/protected.tsx import { redirect, useRouteError } from "react-router-dom"; export async function protectedLoader() { const isAuthed = await getSession(); // your auth check if (!isAuthed) return redirect("/login?reason=unauthorized"); return null; } export function ProtectedPage() { return <h1>Authed Content</h1>; } export function ProtectedErrorBoundary() { const err = useRouteError(); return <p>Something went wrong: {String(err)}</p>; }
Route config:
{ path: "protected", loader: protectedLoader, element: <ProtectedPage />, errorElement: <ProtectedErrorBoundary />, }
This never renders the protected component when unauthorized; users land straight on /login
with no content flash.
3) Relative & Dynamic Navigation (Pro Tips)
Relative paths
Inside nested routes, relative paths keep things decoupled:
// From /projects/:id const navigate = useNavigate(); <button onClick={() => navigate("settings")}>Project Settings</button> // navigates to /projects/:id/settings
Go up one segment:
navigate(".."); // -> parent route navigate("../.."); // -> grandparent
URL params and query strings
navigate(`/users/${userId}?${new URLSearchParams({ tab: "activity" })}`);
Preserve and merge search params
import { useSearchParams } from "react-router-dom"; const [params] = useSearchParams(); const navigate = useNavigate(); const next = new URLSearchParams(params); next.set("page", String(Number(params.get("page") ?? 1) + 1)); navigate({ pathname: "/inbox", search: next.toString() });
4) replace
vs default push
- Push (default):
navigate("/path")
adds a history entry (Back returns to the previous page). - Replace:
navigate("/path", { replace: true })
swaps the current entry → Use for post‑login, post‑logout, or when the previous page shouldn’t be re‑visited.
5) Common Pitfalls & How to Avoid Them
1) Importing from react-router
in web apps
Use react-router-dom
. The DOM package re‑exports the components you want for the browser.
2) Mutating URLSearchParams
in place
Clone before editing to avoid stale reads:
setSearchParams(prev => { const clone = new URLSearchParams(prev); clone.set("page", "1"); return clone; });
3) Redirects inside components that flicker
If a redirect is unconditional and known before render, use a loader redirect (redirect()
), not <Navigate />
in the component.
4) Navigation in unmounted components
Wrap async handlers and check isMounted
or cancel promises to avoid calling navigate
after unmount.
6) Bonus: Action that redirects after a form submit
// routes/contact.tsx import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom"; export async function contactAction({ request }: ActionFunctionArgs) { const form = await request.formData(); await saveMessage({ email: String(form.get("email")), body: String(form.get("body")) }); return redirect("/thanks"); } export function Contact() { const data = useActionData() as { error?: string } | undefined; return ( <Form method="post"> <input name="email" type="email" required /> <textarea name="body" required /> <button type="submit">Send</button> {data?.error && <p role="alert">{data.error}</p>} </Form> ); }
Route:
{ path: "contact", action: contactAction, element: <Contact /> }
Conclusion
- For button click navigation, reach for
useNavigate()
. - For pre‑render logic (auth, post‑submit), use
redirect()
in loaders/actions. - Always import browser bindings from
react-router-dom
. - Prefer relative navigation to reduce coupling in nested routes.
This setup scales cleanly from toy demos to production apps.
✍️ Written by: Cristian Sifuentes --- Full-stack developer & AI
enthusiast, passionate about building scalable architectures and
teaching dev teams how to thrive in the age of AI.
Top comments (0)