Skip to content

React Router v6 - Programmatic Navigation

Problem Statement

When upgrading to React Router v6, many developers encounter issues with programmatic navigation. The common scenario is that this.props.history.push() no longer works as it did in previous versions. Instead, you might see the URL change in the browser's address bar, but the component doesn't actually render the new route.

This occurs because React Router v6 introduced significant changes to its API, including the removal of the history object from props in favor of the new useNavigate hook.

Solutions

The primary solution for programmatic navigation in React Router v6 is the useNavigate hook. This hook returns a function that you can use to navigate programmatically.

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

function MyComponent() {
  const navigate = useNavigate();
  
  const handleClick = () => {
    // Navigate to a specific path
    navigate('/users');
    
    // Navigate with state
    navigate('/users', { state: { user: userData } });
    
    // Navigate backwards/forwards in history
    navigate(-1); // Go back one page
    navigate(1);  // Go forward one page
    navigate(-2); // Go back two pages
  };
  
  return (
    <button onClick={handleClick}>
      Go to Users
    </button>
  );
}

INFO

The useNavigate hook can only be used inside functional components. If you're working with class components, see the alternative solutions below.

2. Accessing Navigation in Class Components

For class components, you have several options:

Option A: Create a Higher-Order Component (HOC)

jsx
// withRouter.js
import React from 'react';
import { useNavigate } from 'react-router-dom';

const withRouter = WrappedComponent => props => {
  const navigate = useNavigate();
  return <WrappedComponent {...props} navigate={navigate} />;
};

export default withRouter;
jsx
// YourClassComponent.js
import React, { Component } from 'react';
import withRouter from './withRouter';

class YourClassComponent extends Component {
  handleSubmit = () => {
    // Use navigate from props
    this.props.navigate('/target-route');
  };
  
  render() {
    return (
      // Your component JSX
    );
  }
}

export default withRouter(YourClassComponent);

Option B: Wrap Class Component with Functional Component

jsx
import React from 'react';
import { useNavigate } from 'react-router-dom';
import YourClassComponent from './YourClassComponent';

export default function(props) {
  const navigate = useNavigate();
  return <YourClassComponent {...props} navigate={navigate} />;
}

3. Global Navigation with Custom Router

If you need to access navigation outside of React components, you can create a custom router:

jsx
// CustomRouter.js
import React from 'react';
import { Router } from 'react-router-dom';
import { BrowserHistory } from 'history';

const CustomRouter = ({ basename, children, history }) => {
  const [state, setState] = React.useState({
    action: history.action,
    location: history.location,
  });

  React.useLayoutEffect(() => history.listen(setState), [history]);

  return (
    <Router
      basename={basename}
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
  );
};

export default CustomRouter;
jsx
// App.js
import { createBrowserHistory } from 'history';
import CustomRouter from './CustomRouter';

const history = createBrowserHistory();

function App() {
  return (
    <CustomRouter history={history}>
      {/* Your app components */}
    </CustomRouter>
  );
}

// Now you can use history.push() anywhere
history.push('/target-route');

4. Navigation Setup for External Functions

If you need to navigate from non-React code (like API callbacks):

jsx
// navigation.js
import { useNavigate } from 'react-router-dom';

let navigator;

export const useNavigationSetup = () => {
  navigator = useNavigate();
};

export const navigate = (to) => {
  if (navigator) {
    navigator(to);
  }
};
jsx
// App.js
import { useNavigationSetup } from './navigation';

function App() {
  useNavigationSetup();
  // Your app components
}
jsx
// api.js
import { navigate } from './navigation';

export const loginUser = async (credentials) => {
  try {
    const response = await axios.post('/login', credentials);
    navigate('/dashboard');
    return response;
  } catch (error) {
    // Handle error
  }
};

Common Pitfalls and Solutions

WARNING

Don't create multiple history instances. In your original code, you were creating a new createBrowserHistory() instance inside your handler, which isn't connected to your router.

Incorrect approach:

jsx
// This won't work properly
handleSubmit() {
  const history = createBrowserHistory(); // Creates isolated history
  history.push('/users'); // Not connected to your router
}

Correct approach:

jsx
// Use the navigate function from useNavigate
handleSubmit() {
  navigate('/users'); // Using the hook-provided navigate function
}

Migration from v5 to v6

If you're migrating from React Router v5, here's the main change:

v5 code:

jsx
import { useHistory } from 'react-router-dom';

function App() {
  let history = useHistory();
  
  function handleClick() {
    history.push('/home');
  }
  
  return (
    <button onClick={handleClick}>Go home</button>
  );
}

v6 code:

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

function App() {
  let navigate = useNavigate();
  
  function handleClick() {
    navigate('/home');
  }
  
  return (
    <button onClick={handleClick}>Go home</button>
  );
}

Conclusion

React Router v6 simplifies navigation with the useNavigate hook, which replaces the previous useHistory approach. For most use cases, especially in functional components, useNavigate is the recommended solution. For class components or external navigation needs, use the higher-order component pattern or a custom router setup.

Remember that the key difference in v6 is that navigation is now primarily handled through the useNavigate hook rather than direct manipulation of the history object.