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)