| |

React 61 ⚛️ Programmatic Navigation

⏱️ Estimated read: 18 min | |

A link is a declaration: the user clicks, and the router navigates. But not every navigation is a click. A form submits, a login succeeds, an OAuth callback lands, a timer fires, and the application must move the user to a different route without a link in sight. Programmatic navigation is the imperative side of the router, and React Router exposes it through the useNavigate hook. The hook returns a function that changes the location, and the function accepts a path, a delta, or a location object, with options that control the history stack and the state that travels with the navigation.

Key point: useNavigate returns a function that must be called from an event handler, an effect, or another callback. Calling it during render is a bug. The function is stable across renders, so it can be stored in a variable or passed to a child without causing re-renders.


Why programmatic navigation exists

The event problem. A navigation is often the result of an event that is not a link click. A form submits successfully, and the application must redirect to the dashboard. A button triggers a logout, and the user must be sent to the login page. The useNavigate hook provides the imperative call.

The condition problem. The destination depends on the outcome. A login form submits, the server returns the user’s role, and the application navigates to the admin panel or the user dashboard. The branch is in the code, not in the markup.

The redirect problem. A protected route renders, the authentication check fails, and the component must redirect to the login page. The <Navigate> component is the declarative form, and the useNavigate hook is the imperative form, and both are used in different contexts.

The history problem. The back button, the forward button, and the browser history are part of the navigation model. The navigate(-1) call moves the user back, and the replace: true option replaces the current entry instead of adding a new one.

The state problem. A navigation can carry state that is not in the URL. The login form passes the user’s intended destination, and the login page reads it after the authentication. The state option carries the data, and the useLocation hook reads it.

The callback problem. An OAuth provider redirects the user to a callback URL, and the callback component must parse the token and navigate to the application. The navigation is the result of the callback, not a user click.

The timer problem. A session expires after a period of inactivity, and the application must navigate to the login page. The timer fires, and the navigation happens without user interaction.


a. The useNavigate hook

The useNavigate hook returns the navigate function. The function is called with a destination and an optional options object.

The basic usage

import { useNavigate } from 'react-router-dom';

function LoginButton() {
  const navigate = useNavigate();

  function handleClick() {
    navigate('/dashboard');
  }

  return <button onClick={handleClick}>Log in</button>;
}

The navigate function changes the route to /dashboard. The current history entry is replaced by the new one.

The path

The first argument is the destination. It can be a string path or a location object.

navigate('/dashboard');
navigate('/users/42');
navigate('/users/42?tab=profile');

The string is parsed and matched against the route configuration.

The location object

The destination can be a location object with a pathname, a search, and a hash.

navigate({
  pathname: '/dashboard',
  search: '?tab=profile',
  hash: '#section',
});

The object form is more explicit, and it is the form the useLocation hook returns.

The delta

The argument can be a number, which moves the user backward or forward in the history.

navigate(-1);  // go back
navigate(1);   // go forward
navigate(-2);  // go back two entries

The number is the delta. The -1 is the same as the back button.

The replace option

The replace option replaces the current entry instead of adding a new one.

navigate('/login', { replace: true });

After the navigation, the back button returns to the page before the current one, not to the page that issued the navigation. The option is used after a login, a logout, or a redirect, where returning to the previous page is not wanted.

The state option

The state option carries data that is not in the URL.

navigate('/login', { state: { from: '/settings' } });

The state is stored in the history entry, and the useLocation hook reads it in the destination.

function LoginPage() {
  const location = useLocation();
  const from = location.state?.from ?? '/';

  function handleLogin() {
    // Authenticate, then navigate to the original destination.
    navigate(from, { replace: true });
  }

  return <button onClick={handleLogin}>Log in</button>;
}

The state is the mechanism for the “return to the original page after login” pattern.

The relative navigation

The destination can be relative to the current route.

navigate('settings');    // relative to the current path
navigate('../settings'); // up one level, then settings
navigate('./settings');  // explicit current level

The relative form is useful inside a nested route.

The -1 delta and the state

The navigate(-1) call does not carry state. It returns to the previous entry exactly as it was, including its state.


b. The common patterns

The useNavigate hook is used in a small set of patterns. The patterns are the reason the hook exists.

The login redirect

A login form navigates to the dashboard after a successful authentication.

function LoginForm() {
  const navigate = useNavigate();
  const [email, setEmail] = useState('');

  async function handleSubmit(e) {
    e.preventDefault();
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email }),
    });
    if (response.ok) {
      navigate('/dashboard', { replace: true });
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <button type="submit">Log in</button>
    </form>
  );
}

The replace: true prevents the user from returning to the login form with the back button.

The logout redirect

A logout button navigates to the login page after clearing the session.

function LogoutButton() {
  const navigate = useNavigate();

  function handleLogout() {
    localStorage.removeItem('token');
    navigate('/login', { replace: true });
  }

  return <button onClick={handleLogout}>Log out</button>;
}

The protected route

A component checks the authentication and redirects if the check fails.

function ProtectedRoute({ children }) {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}

The <Navigate> component is the declarative form. The useNavigate hook is the imperative form. The <Navigate> is used inside a render, and the useNavigate is used inside a handler.

The form submit

A form submits to a server, and the result determines the destination.

function CreatePostForm() {
  const navigate = useNavigate();

  async function handleSubmit(e) {
    e.preventDefault();
    const data = new FormData(e.target);
    const post = await createPost(data);
    navigate(`/posts/${post.id}`);
  }

  return <form onSubmit={handleSubmit}>...</form>;
}

The destination is the new resource’s URL.

The post-login return

The login page navigates to the original destination after a successful authentication.

function LoginPage() {
  const navigate = useNavigate();
  const location = useLocation();
  const from = location.state?.from?.pathname ?? '/dashboard';

  async function handleLogin(credentials) {
    await login(credentials);
    navigate(from, { replace: true });
  }

  return <LoginForm onSubmit={handleLogin} />;
}

The location.state.from is the original location, and the replace: true prevents the user from returning to the login page.

The OAuth callback

The OAuth callback component parses the token and navigates.

function OAuthCallback() {
  const navigate = useNavigate();
  const [searchParams] = useSearchParams();

  useEffect(() => {
    const token = searchParams.get('token');
    if (token) {
      localStorage.setItem('token', token);
      navigate('/dashboard', { replace: true });
    } else {
      navigate('/login', { replace: true });
    }
  }, [navigate, searchParams]);

  return <p>Authenticating...</p>;
}

The effect runs once, and the navigation happens after the token is stored.

The conditional redirect

A component redirects based on a condition.

function Dashboard() {
  const { user } = useAuth();
  const navigate = useNavigate();

  useEffect(() => {
    if (!user) {
      navigate('/login', { replace: true });
    }
  }, [user, navigate]);

  if (!user) return null;
  return <div>Welcome, {user.name}</div>;
}

The effect runs when the user changes, and the navigation happens if the user is not authenticated.

The wizard

A wizard navigates to the next step after the current step is complete.

function WizardStep({ step }) {
  const navigate = useNavigate();

  function handleNext() {
    navigate(`/wizard/${step + 1}`);
  }

  function handleBack() {
    navigate(`/wizard/${step - 1}`);
  }

  return (
    <div>
      <button onClick={handleBack} disabled={step === 1}>Back</button>
      <button onClick={handleNext}>Next</button>
    </div>
  );
}

The search form

A search form navigates to the results page with the query string.

function SearchForm() {
  const navigate = useNavigate();
  const [query, setQuery] = useState('');

  function handleSubmit(e) {
    e.preventDefault();
    navigate(`/search?q=${encodeURIComponent(query)}`);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <button type="submit">Search</button>
    </form>
  );
}

The query is encoded and passed in the search string.

The session timeout

A timer navigates to the login page after a period of inactivity.

function SessionTimeout({ timeout = 30 * 60 * 1000 }) {
  const navigate = useNavigate();

  useEffect(() => {
    const timer = setTimeout(() => {
      navigate('/login', { replace: true });
    }, timeout);
    return () => clearTimeout(timer);
  }, [navigate, timeout]);

  return null;
}

c. The caveats and the common mistakes

The useNavigate hook has specific rules. The rules prevent the subtle bugs.

The render rule

The navigate function must not be called during render. The call is a side effect, and the render must be pure.

// ❌ Wrong: navigate during render
function Component() {
  const navigate = useNavigate();
  navigate('/dashboard');
  return <div>...</div>;
}

// ✅ Correct: navigate in an effect
function Component() {
  const navigate = useNavigate();
  useEffect(() => {
    navigate('/dashboard');
  }, [navigate]);
  return <div>...</div>;
}

The effect runs after the render, and the navigation is the side effect.

The <Navigate> alternative

The <Navigate> component is the declarative form of the same operation. It is used inside a render, and the navigation happens when the component renders.

function ProtectedRoute({ children }) {
  const { user } = useAuth();
  if (!user) return <Navigate to="/login" replace />;
  return children;
}

The two forms are equivalent. The <Navigate> is for the render-time redirect, and the useNavigate is for the event-handler redirect.

The stale closure

The navigate function is stable, but the values it captures are not. The useEffect dependency array must include the values the effect uses.

useEffect(() => {
  if (!user) navigate('/login', { replace: true });
}, [user, navigate]);

The user is in the dependency array. The effect re-runs when the user changes.

The double navigation

A component that navigates in both the render and the effect navigates twice. The render-time navigation is the bug.

// ❌ Wrong: navigates twice
function Component() {
  const navigate = useNavigate();
  if (!user) navigate('/login');  // Render
  useEffect(() => {
    if (!user) navigate('/login');  // Effect
  }, [user, navigate]);
}

The relative navigation

The relative navigation depends on the current route. The navigate('settings') inside /users/42 goes to /users/settings, not /settings.

// From /users/42
navigate('settings');    // /users/settings
navigate('../settings'); // /settings
navigate('/settings');   // /settings (absolute)

The leading slash makes the path absolute.

The state is not in the URL

The state option is not visible in the URL. The state is lost when the page is refreshed, and the state is not shareable.

navigate('/login', { state: { from: '/settings' } });
// The URL is /login, not /login?from=/settings

The state is the mechanism for the transient data, and the query string is the mechanism for the persistent data.

The replace vs the push

The replace: true replaces the current entry, and the default is the push. The replace is the right choice for a redirect, a login, a logout, and a 404. The push is the right choice for a link.

// Replace: the back button does not return here.
navigate('/login', { replace: true });

// Push: the back button returns here.
navigate('/dashboard');

The useNavigate in a class component

A class component cannot use the hook. The withRouter HOC or the <Navigate> component is the alternative, and the functional component is the recommended approach.

The caveat table

CaveatRule
RenderDo not call navigate during render
EffectUse an effect for the render-time redirect
<Navigate>The declarative alternative
DependenciesInclude the values in the dependency array
RelativeThe leading slash makes the path absolute
StateNot in the URL, not shareable
ReplaceUse for redirects, not for links

Complete Example Session

// ============================================
// PART 1: THE BASIC NAVIGATE
// ============================================

import { useNavigate } from 'react-router-dom';

function LoginButton() {
  const navigate = useNavigate();
  return <button onClick={() => navigate('/dashboard')}>Log in</button>;
}

// ============================================
// PART 2: THE REPLACE OPTION
// ============================================

function LogoutButton() {
  const navigate = useNavigate();

  function handleLogout() {
    localStorage.removeItem('token');
    navigate('/login', { replace: true });
  }

  return <button onClick={handleLogout}>Log out</button>;
}

// ============================================
// PART 3: THE STATE OPTION
// ============================================

function SettingsPage() {
  const navigate = useNavigate();

  function handleLogin() {
    navigate('/login', { state: { from: '/settings' } });
  }

  return <button onClick={handleLogin}>Log in to edit</button>;
}

function LoginPage() {
  const navigate = useNavigate();
  const location = useLocation();
  const from = location.state?.from ?? '/';

  function handleSuccess() {
    navigate(from, { replace: true });
  }

  return <button onClick={handleSuccess}>Log in</button>;
}

// ============================================
// PART 4: THE DELTA
// ============================================

function BackButton() {
  const navigate = useNavigate();
  return <button onClick={() => navigate(-1)}>Back</button>;
}

// ============================================
// PART 5: THE PROTECTED ROUTE
// ============================================

function ProtectedRoute({ children }) {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}

// ============================================
// PART 6: THE FORM SUBMIT
// ============================================

function CreatePostForm() {
  const navigate = useNavigate();

  async function handleSubmit(e) {
    e.preventDefault();
    const data = new FormData(e.target);
    const post = await createPost(data);
    navigate(`/posts/${post.id}`);
  }

  return <form onSubmit={handleSubmit}>...</form>;
}

// ============================================
// PART 7: THE OAUTH CALLBACK
// ============================================

function OAuthCallback() {
  const navigate = useNavigate();
  const [searchParams] = useSearchParams();

  useEffect(() => {
    const token = searchParams.get('token');
    if (token) {
      localStorage.setItem('token', token);
      navigate('/dashboard', { replace: true });
    } else {
      navigate('/login', { replace: true });
    }
  }, [navigate, searchParams]);

  return <p>Authenticating...</p>;
}

// ============================================
// PART 8: THE CONDITIONAL REDIRECT
// ============================================

function Dashboard() {
  const { user } = useAuth();
  const navigate = useNavigate();

  useEffect(() => {
    if (!user) navigate('/login', { replace: true });
  }, [user, navigate]);

  if (!user) return null;
  return <div>Welcome, {user.name}</div>;
}

// ============================================
// PART 9: THE SESSION TIMEOUT
// ============================================

function SessionTimeout({ timeout = 30 * 60 * 1000 }) {
  const navigate = useNavigate();

  useEffect(() => {
    const timer = setTimeout(() => {
      navigate('/login', { replace: true });
    }, timeout);
    return () => clearTimeout(timer);
  }, [navigate, timeout]);

  return null;
}

// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================

// 1. Call navigate in an event handler or an effect.
// 2. Use the replace option for redirects.
// 3. Use the state option for the transient data.
// 4. Use the delta for the back and forward.
// 5. Use <Navigate> for the render-time redirect.
// 6. Include the dependencies in the effect array.
// 7. Use the leading slash for the absolute paths.
// 8. Do not call navigate during render.

The ten parts showed the basic navigate, the replace option, the state option, the delta, the protected route, the form submit, the OAuth callback, the conditional redirect, the session timeout, and the complete pattern.


Quick Reference

The useNavigate API

ArgumentTypePurpose
toString or objectThe destination
options.replaceBooleanReplace the history entry
options.stateAnyThe transient data
options.relative'route' | 'path'The relative resolution

The Destination Forms

FormExample
String pathnavigate('/dashboard')
Objectnavigate({ pathname: '/dashboard' })
Deltanavigate(-1)
Relativenavigate('settings')
Absolutenavigate('/settings')

The Common Patterns

PatternOptions
Login redirectreplace: true
Logout redirectreplace: true
Post-login returnstate: { from }, replace: true
Back buttonnavigate(-1)
Search formnavigate('/search?q=...')

The useNavigate vs <Navigate>

AspectuseNavigate<Navigate>
Used inEvent handler, effectRender
FormImperativeDeclarative
Use caseAfter an eventDuring a render
RedirectYesYes

The Caveats

CaveatRule
RenderDo not call during render
DependenciesInclude the values
StateNot in the URL
RelativeThe leading slash is absolute
ReplaceUse for redirects

Best Practices

✅ Do This:

// Call navigate in an event handler.
function handleClick() { navigate('/dashboard'); }  // ✅

// Call navigate in an effect.
useEffect(() => { if (!user) navigate('/login'); }, [user, navigate]);  // ✅

// Use the replace option for the login redirect.
navigate('/dashboard', { replace: true });  // ✅

// Use the state option for the return destination.
navigate('/login', { state: { from: location } });  // ✅

// Use the delta for the back button.
navigate(-1);  // ✅

// Use <Navigate> for the render-time redirect.
if (!user) return <Navigate to="/login" replace />;  // ✅

// Use the leading slash for the absolute path.
navigate('/settings');  // ✅

❌ Don’t Do This:

// Don't call navigate during render.
function Component() {
  const navigate = useNavigate();
  navigate('/dashboard');  // ❌
  return <div>...</div>;
}

// Don't forget the dependencies.
useEffect(() => {
  if (!user) navigate('/login');
}, []);  // ❌ The user is stale

// Don't use the state for the persistent data.
navigate('/search', { state: { q: 'react' } });  // ❌ Use the query string

// Don't use the push for the redirect.
navigate('/login');  // ❌ The back button returns to the form

// Don't forget the leading slash for the absolute path.
navigate('settings');  // ⚠️ Relative to the current path

// Don't navigate in both the render and the effect.
if (!user) navigate('/login');  // ❌ Double navigation

Common Pitfalls

PitfallWhy It HappensFix
Navigation on renderThe call is a side effectUse an effect
Double navigationThe render and the effectRemove the render call
Stale closureThe dependency array is emptyAdd the values
Back button returns to the formNo replace: trueAdd the option
State lost on refreshThe state is not in the URLUse the query string
Relative path surpriseThe leading slash is missingAdd the slash
useNavigate in a classThe hook is for functionsUse <Navigate>

Real-World Examples

1. Basic Navigate

navigate('/dashboard');

2. Replace

navigate('/login', { replace: true });

3. State

navigate('/login', { state: { from: location } });

4. Delta

navigate(-1);

5. Login Redirect

navigate('/dashboard', { replace: true });

6. Post-Login Return

navigate(from, { replace: true });

7. Protected Route

if (!user) return <Navigate to="/login" replace />;

8. OAuth Callback

useEffect(() => { navigate('/dashboard', { replace: true }); }, []);

9. Session Timeout

setTimeout(() => navigate('/login', { replace: true }), timeout);

10. Search Form

navigate(`/search?q=${encodeURIComponent(query)}`);

Visual

The useNavigate Flow

┌────────────────────────────────────────────────────────────┐
│  THE useNavigate FLOW                                      │
│                                                            │
│  EVENT                                                     │
│  ┌──────────────────────────────────────────────────┐     │
│  │  <button onClick={handleClick}>                   │     │
│  │    → handleClick()                                │     │
│  │      → navigate('/dashboard')                     │     │
│  │        → the router changes the location          │     │
│  │        → the matched component renders            │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  EFFECT                                                    │
│  ┌──────────────────────────────────────────────────┐     │
│  │  useEffect(() => {                                │     │
│  │    if (!user) navigate('/login');                 │     │
│  │  }, [user, navigate]);                            │     │
│  │                                                   │     │
│  │  The effect runs after the render.                │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Replace vs Push

┌────────────────────────────────────────────────────────────┐
│  REPLACE vs PUSH                                           │
│                                                            │
│  PUSH (default)                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  History: [/, /login, /dashboard]                 │     │
│  │                                                   │     │
│  │  The back button returns to /login.               │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  REPLACE                                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  History: [/, /dashboard]                         │     │
│  │                                                   │     │
│  │  The /login entry is replaced.                    │     │
│  │  The back button returns to /.                    │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The replace is the right choice for a redirect.           │
└────────────────────────────────────────────────────────────┘

The State

┌────────────────────────────────────────────────────────────┐
│  THE STATE                                                 │
│                                                            │
│  SOURCE: /settings                                         │
│  ┌──────────────────────────────────────────────────┐     │
│  │  navigate('/login', { state: { from: '/settings' } });│  │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  DESTINATION: /login                                       │
│  ┌──────────────────────────────────────────────────┐     │
│  │  const location = useLocation();                  │     │
│  │  const from = location.state?.from;               │     │
│  │                                                   │     │
│  │  from === '/settings'                             │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  AFTER LOGIN: /settings                                    │
│  ┌──────────────────────────────────────────────────┐     │
│  │  navigate(from, { replace: true });               │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The useNavigate vs Navigate

┌────────────────────────────────────────────────────────────┐
│  useNavigate vs <Navigate>                                 │
│                                                            │
│  useNavigate                                               │
│  ┌──────────────────────────────────────────────────┐     │
│  │  function handleClick() {                         │     │
│  │    navigate('/dashboard');                        │     │
│  │  }                                                │     │
│  │                                                   │     │
│  │  Imperative. Used in an event handler.            │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  <Navigate>                                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  if (!user) return <Navigate to="/login" replace />;│    │
│  │                                                   │     │
│  │  Declarative. Used during a render.               │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The two are equivalent. Use the right one for the context.│
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
useNavigateThe hook that returns the navigate function
navigate(to)The basic navigation
navigate(-1)The back navigation
replace: trueReplace the history entry
state: {...}The transient data
<Navigate>The declarative alternative
Relative pathNo leading slash
Absolute pathLeading slash
useLocationReads the state in the destination
EffectThe right place for the render-time redirect

Key takeaways:

  • useNavigate returns the navigate function. The function is called with a destination and an options object. The destination can be a string, a location object, or a delta. The function is stable across renders.
  • The navigate function must not be called during render. The call is a side effect. The event handler and the effect are the right places. The render-time redirect uses the <Navigate> component.
  • The replace option replaces the history entry. The option is used after a login, a logout, a redirect, and a 404. The default is the push, which adds a new entry.
  • The state option carries the transient data. The data is not in the URL, and the destination reads it with the useLocation hook. The “return to the original page after login” pattern uses the state.
  • The relative path is relative to the current route. The navigate('settings') inside /users/42 goes to /users/settings. The leading slash makes the path absolute.
  • The <Navigate> component is the declarative form. It is used inside a render, and the navigation happens when the component renders. The useNavigate hook is used inside an event handler or an effect.
  • The dependency array must include the values. The useEffect that navigates based on the user must include the user and the navigate in the array. The missing dependency produces the stale closure.
  • The double navigation is the common bug. The component that navigates in both the render and the effect navigates twice. The render call is the bug.

Remember: The useNavigate hook is the imperative side of React Router. It returns the navigate function, which is called from an event handler or an effect. The function accepts a path, a delta, or a location object, and the options control the history stack and the state. The <Navigate> component is the declarative alternative for the render-time redirect. The result is an application that moves the user through the routes in response to the events, the conditions, and the timeouts, and not only to the clicks.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!