# React Router useHistory in Functional Components: Best Practices to Avoid Unexpected Behavior

> Learn best practices for implementing React Router's useHistory in functional components. Avoid unexpected behavior with useCallback and proper dependency arrays.

- Repository: [Meta/react](https://github.com/facebook/react)
- Tags: best-practices
- Published: 2026-02-18

---

**Always use the `useHistory` hook (React Router v5) or `useNavigate` hook (React Router v6) inside `useCallback` with proper dependency arrays to prevent stale closures and ensure navigation state stays synchronized with React's rendering cycle.**

Programmatic navigation in React functional components requires careful integration with the framework's lifecycle to avoid synchronization issues. When implementing **react router useHistory** patterns in the facebook/react ecosystem, you must avoid direct history object manipulation to prevent hydration mismatches and broken transition blockers.

## Why Direct History Manipulation Causes Bugs

Accessing `window.history` or importing the raw history object directly bypasses React Router's internal state management. According to the facebook/react source code, this creates three critical issues:

- **Stale closures**: Callbacks capturing an outdated history reference may navigate to wrong URLs or lose state updates
- **SSR incompatibility**: Direct browser API usage breaks server-side rendering hydration, as noted in [`packages/react-server/README.md`](https://github.com/facebook/react/blob/main/packages/react-server/README.md)
- **Broken router features**: Features like navigation blockers and `<Prompt>` components rely on the router's internal state tracking in `packages/react-dom`

## Implementing Navigation in React Router v5 with useHistory

For applications using React Router v5, the `useHistory` hook provides a stable history object that persists across renders.

```tsx
import { useHistory, useLocation } from 'react-router-dom';
import { useCallback } from 'react';

function NavigationComponent() {
  const history = useHistory();
  const { pathname } = useLocation();

  const goToDashboard = useCallback(() => {
    history.push('/dashboard');
  }, [history]);

  const goToProfile = useCallback((userId: string) => {
    history.push({
      pathname: `/profile/${userId}`,
      state: { from: pathname },
    });
  }, [history, pathname]);

  return (
    <>
      <button onClick={goToDashboard}>Dashboard</button>
      <button onClick={() => goToProfile('123')}>Profile</button>
    </>
  );
}

```

The `history` object returned by **react router useHistory** maintains a stable reference, making it safe to include in `useCallback` dependency arrays without causing unnecessary re-renders.

## React Router v6: Migrating to useNavigate

React Router v6 replaced `useHistory` with `useNavigate`, which returns a stable function reference optimized for concurrent rendering.

```tsx
import { useNavigate, useLocation } from 'react-router-dom';
import { useCallback } from 'react';

function NavigationComponent() {
  const navigate = useNavigate();
  const { pathname } = useLocation();

  const goToDashboard = useCallback(() => {
    navigate('/dashboard', { replace: false });
  }, [navigate]);

  const goToProfile = useCallback((userId: string) => {
    navigate(`/profile/${userId}`, { state: { from: pathname } });
  }, [navigate, pathname]);

  return (
    <div>
      <button onClick={goToDashboard}>Dashboard</button>
      <button onClick={() => goToProfile('123')}>Profile</button>
    </div>
  );
}

```

## Common Pitfalls and Solutions

When working with **react router useHistory**, avoid these patterns that cause unexpected behavior:

**Calling history.push inside useEffect without dependencies**

Navigation effects that run only once may use stale route information. Always include `history` and route-dependent values in the dependency array, or move navigation to event handlers.

**Mixing replace and push unintentionally**

This creates confusing back-button behavior. Explicitly set `replace: true` only when overwriting the current history entry, such as during authentication redirects.

**Navigating after component unmount**

Asynchronous callbacks may fire after unmount, causing memory leak warnings. Cancel async operations in cleanup functions or guard with a mounted flag.

**Using window.history instead of the hook**

This breaks React Router's internal state synchronization. The [`packages/react-reconciler/src/__tests__/useEffectEvent-test.js`](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/__tests__/useEffectEvent-test.js) file demonstrates how stale closures affect React's effect system, which applies directly to navigation callbacks.

## Testing Navigation with MemoryRouter

Unit tests should verify navigation behavior using `MemoryRouter` to isolate routing state.

```tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import NavigationComponent from './NavigationComponent';

test('navigates to dashboard on button click', () => {
  render(
    <MemoryRouter initialEntries={['/']}>
      <Routes>
        <Route path="/" element={<NavigationComponent />} />
        <Route path="/dashboard" element={<div>Dashboard page</div>} />
      </Routes>
    </MemoryRouter>
  );

  fireEvent.click(screen.getByText(/Dashboard/i));
  expect(screen.getByText(/Dashboard page/i)).toBeInTheDocument();
});

```

## Key React Source Files for Navigation Context

Understanding these files from the facebook/react repository helps explain why proper hook usage matters:

- **[`packages/react/README.md`](https://github.com/facebook/react/blob/main/packages/react/README.md)**: Documents the rendering model that hooks like `useEffect` depend on for proper execution timing
- **[`packages/react-dom/README.md`](https://github.com/facebook/react/blob/main/packages/react-dom/README.md)**: Contains the DOM renderer implementation used by React Router for browser history manipulation
- **[`packages/scheduler/README.md`](https://github.com/facebook/react/blob/main/packages/scheduler/README.md)**: Powers concurrent rendering features that affect when navigation side-effects execute
- **[`packages/eslint-plugin-react-hooks/README.md`](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/README.md)**: Enforces correct hook dependencies, preventing missing `history` or `navigate` from dependency arrays
- **[`packages/react-server/README.md`](https://github.com/facebook/react/blob/main/packages/react-server/README.md)**: Explains SSR constraints that make direct `window.history` usage problematic during hydration

## Summary

- **Use router hooks exclusively**: Always choose `useHistory` (v5) or `useNavigate` (v6) over raw history objects to maintain React's internal state synchronization
- **Memoize navigation handlers**: Wrap navigation functions in `useCallback` with proper dependency arrays to prevent stale closures
- **Include proper dependencies**: Add `history`, `navigate`, and location values to effect dependency arrays when navigation occurs inside `useEffect`
- **Test with MemoryRouter**: Isolate navigation logic in unit tests using `MemoryRouter` rather than `BrowserRouter`
- **Respect SSR constraints**: Avoid direct `window.history` access to prevent hydration mismatches documented in `packages/react-server`

## Frequently Asked Questions

### Why does my navigation callback redirect to the wrong URL?

This typically occurs due to stale closures where your callback captures an outdated history reference. Always use the `useHistory` or `useNavigate` hook inside `useCallback` with the hook in the dependency array, ensuring you access the current navigation function on each render.

### Can I use useHistory in React Router v6?

No, React Router v6 removed `useHistory` in favor of `useNavigate`. If you are migrating from v5, replace `history.push('/path')` with `navigate('/path')` and `history.replace('/path')` with `navigate('/path', { replace: true })`.

### How do I prevent navigation if my component unmounts?

Cancel any asynchronous operations that trigger navigation in your effect cleanup functions. Alternatively, use a mounted flag pattern or AbortController to guard navigation calls inside async callbacks, preventing state updates on unmounted components.

### Why does direct window.history usage break my React app?

Direct `window.history` manipulation bypasses React Router's internal state tracking, breaking features like navigation blockers and causing SSR hydration mismatches. As implemented in `packages/react-reconciler`, React expects to control the navigation state to properly schedule updates and transitions.