DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

React Router: Navigate on Button Click with createBrowserRouter (TypeScript, Pro Patterns)

React Router: Navigate on Button Click with createBrowserRouter (TypeScript, Pro Patterns)

Heads‑up: In browser apps, import from react-router-dom, not react-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> ); 
Enter fullscreen mode Exit fullscreen mode

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> ); } 
Enter fullscreen mode Exit fullscreen mode

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> ); } 
Enter fullscreen mode Exit fullscreen mode

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> ); } 
Enter fullscreen mode Exit fullscreen mode

routes/NotFound.tsx

export function NotFound() { return <h1>404 — Not Found</h1>; } 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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>; } 
Enter fullscreen mode Exit fullscreen mode

Route config:

{ path: "protected", loader: protectedLoader, element: <ProtectedPage />, errorElement: <ProtectedErrorBoundary />, } 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Go up one segment:

navigate(".."); // -> parent route navigate("../.."); // -> grandparent 
Enter fullscreen mode Exit fullscreen mode

URL params and query strings

navigate(`/users/${userId}?${new URLSearchParams({ tab: "activity" })}`); 
Enter fullscreen mode Exit fullscreen mode

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() }); 
Enter fullscreen mode Exit fullscreen mode

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; }); 
Enter fullscreen mode Exit fullscreen mode

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> ); } 
Enter fullscreen mode Exit fullscreen mode

Route:

{ path: "contact", action: contactAction, element: <Contact /> } 
Enter fullscreen mode Exit fullscreen mode

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)