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)