Programmatically Navigate Using React-Router

Programmatically navigate using React router

React Router v5.1.0 with hooks

There is a new useHistory hook in React Router >5.1.0 if you are using React >16.8.0 and functional components.

import { useHistory } from "react-router-dom";

function HomeButton() {
const history = useHistory();

function handleClick() {
history.push("/home");
}

return (
<button type="button" onClick={handleClick}>
Go home
</button>
);
}

React Router v4

With v4 of React Router, there are three approaches that you can take to programmatic routing within components.

  1. Use the withRouter higher-order component.
  2. Use composition and render a <Route>
  3. Use the context.

React Router is mostly a wrapper around the history library. history handles interaction with the browser's window.history for you with its browser and hash histories. It also provides a memory history which is useful for environments that don't have a global history. This is particularly useful in mobile app development (react-native) and unit testing with Node.

A history instance has two methods for navigating: push and replace. If you think of the history as an array of visited locations, push will add a new location to the array and replace will replace the current location in the array with the new one. Typically you will want to use the push method when you are navigating.

In earlier versions of React Router, you had to create your own history instance, but in v4 the <BrowserRouter>, <HashRouter>, and <MemoryRouter> components will create a browser, hash, and memory instances for you. React Router makes the properties and methods of the history instance associated with your router available through the context, under the router object.

1. Use the withRouter higher-order component

The withRouter higher-order component will inject the history object as a prop of the component. This allows you to access the push and replace methods without having to deal with the context.

import { withRouter } from 'react-router-dom'
// this also works with react-router-native

const Button = withRouter(({ history }) => (
<button
type='button'
onClick={() => { history.push('/new-location') }}
>
Click Me!
</button>
))

2. Use composition and render a <Route>

The <Route> component isn't just for matching locations. You can render a pathless route and it will always match the current location. The <Route> component passes the same props as withRouter, so you will be able to access the history methods through the history prop.

import { Route } from 'react-router-dom'

const Button = () => (
<Route render={({ history}) => (
<button
type='button'
onClick={() => { history.push('/new-location') }}
>
Click Me!
</button>
)} />
)

3. Use the context*

But you probably should not

The last option is one that you should only use if you feel comfortable working with React's context model (React's Context API is stable as of v16).

const Button = (props, context) => (
<button
type='button'
onClick={() => {
// context.history.push === history.push
context.history.push('/new-location')
}}
>
Click Me!
</button>
)

// you need to specify the context type so that it
// is available within the component
Button.contextTypes = {
history: React.PropTypes.shape({
push: React.PropTypes.func.isRequired
})
}

1 and 2 are the simplest choices to implement, so for most use cases, they are your best bets.

React router v6 programmatically redirect

When you are on a given route:

<Route path="customerOrders/:id" element={<CustomerOrderForm />} />

and navigating to the same route already rendering the mounted component then the component needs to "listen" for changes to the route, in this case, specifically the id route match param that is updated. Use an useEffect hook with a dependency on the id route match param to rerun any logic depending on it.

import { useNavigate, useParams } from "react-router-dom";

...

const CustomerOrderForm = () => {
const navigate = useNavigate();
const { id } = useParams();

useEffect(() => {
// rerun logic depending on id value
}, [id]);

const save = async () => {
//
// logic to persist data goes here...
//

navigate(`/customerOrders/${customerOrderId}`);
};

...

};

Programmatically navigate using react router V4

If you are targeting browser environments, you need to use react-router-dom package, instead of react-router. They are following the same approach as React did, in order to separate the core, (react) and the platform specific code, (react-dom, react-native ) with the subtle difference that you don't need to install two separate packages, so the environment packages contain everything you need. You can add it to your project as:

yarn add react-router-dom

or

npm i react-router-dom

The first thing you need to do is to provide a <BrowserRouter> as the top most parent component in your application. <BrowserRouter> uses the HTML5 history API and manages it for you, so you don't have to worry about instantiating it yourself and passing it down to the <BrowserRouter> component as a prop (as you needed to do in previous versions).

In V4, for navigating programatically you need to access the history object, which is available through React context, as long as you have a <BrowserRouter> provider component as the top most parent in your application. The library exposes through context the router object, that itself contains history as a property. The history interface offers several navigation methods, such as push, replace and goBack, among others. You can check the whole list of properties and methods here.

Important Note to Redux/Mobx users


If you are using redux or mobx as your state management library in your application, you may have come across issues with components that should be location-aware but are not re-rendered after triggering an URL update

That's happening because react-router passes location to components using the context model.

Both connect and observer create components whose shouldComponentUpdate methods do a shallow comparison of their current props and their next props. Those components will only re-render when at least one prop has changed. This means that in order to ensure they update when the location changes, they will need to be given a prop that changes when the location changes.

The 2 approaches for solving this are:

  • Wrap your connected component in a pathless <Route />. The current location object is one of the props that a <Route> passes to the component it renders
  • Wrap your connected component with the withRouter higher-order component, that in fact has the same effect and injects location as a prop

Setting that aside, there are four ways to navigate programatically, ordered by recommendation:

1.- Using a <Route> Component

It promotes a declarative style. Prior to v4, <Route /> components were placed at the top of your component hierarchy, having to think of your routes structure beforehand. However, now you can have <Route> components anywhere in your tree, allowing you to have a finer control for conditionally rendering depending on the URL. Route injects match, location and history as props into your component. The navigation methods (such as push, replace, goBack...) are available as properties of the history object.

There are 3 ways to render something with a Route, by using either component, render or children props, but don't use more than one in the same Route. The choice depends on the use case, but basically the first two options will only render your component if the path matches the url location, whereas with children the component will be rendered whether the path matches the location or not (useful for adjusting the UI based on URL matching).

If you want to customise your component rendering output, you need to wrap your component in a function and use the render option, in order to pass to your component any other props you desire, apart from match, location and history. An example to illustrate:

import { BrowserRouter as Router } from 'react-router-dom'

const ButtonToNavigate = ({ title, history }) => (
<button
type="button"
onClick={() => history.push('/my-new-location')}
>
{title}
</button>
);

const SomeComponent = () => (
<Route path="/" render={(props) => <ButtonToNavigate {...props} title="Navigate elsewhere" />} />
)

const App = () => (
<Router>
<SomeComponent /> // Notice how in v4 we can have any other component interleaved
<AnotherComponent />
</Router>
);

2.- Using withRouter HoC

This higher order component will inject the same props as Route. However, it carries along the limitation that you can have only 1 HoC per file.

import { withRouter } from 'react-router-dom'

const ButtonToNavigate = ({ history }) => (
<button
type="button"
onClick={() => history.push('/my-new-location')}
>
Navigate
</button>
);

ButtonToNavigate.propTypes = {
history: React.PropTypes.shape({
push: React.PropTypes.func.isRequired,
}),
};

export default withRouter(ButtonToNavigate);

3.- Using a Redirect component

Rendering a <Redirect> will navigate to a new location. But keep in mind that, by default, the current location is replaced by the new one, like server-side redirects (HTTP 3xx). The new location is provided by to prop, that can be a string (URL to redirect to) or a location object. If you want to push a new entry onto the history instead, pass a push prop as well and set it to true

<Redirect to="/your-new-location" push />

4.- Accessing router manually through context

A bit discouraged because context is still an experimental API and it is likely to break/change in future releases of React

const ButtonToNavigate = (props, context) => (
<button
type="button"
onClick={() => context.router.history.push('/my-new-location')}
>
Navigate to a new location
</button>
);

ButtonToNavigate.contextTypes = {
router: React.PropTypes.shape({
history: React.PropTypes.object.isRequired,
}),
};

Needless to say there are also other Router components that are meant to be for non browser ecosystems, such as <NativeRouter> that replicates a navigation stack in memory and targets React Native platform, available through react-router-native package.

For any further reference, don't hesitate to take a look at the official docs. There is also a video made by one of the co-authors of the library that provides a pretty cool introduction to react-router v4, highlighting some of the major changes.

Navigate using react-router-dom v6 after performing user action

In react-router-dom@6 the way to issue imperative navigation actions is to use the navigate function returned from the useNavigate hook. The code you've shared in the snippet is from a class component though, so you'll need to create a Higher Order Component to use the useNavigate hook and inject the navigate function as a prop.

Example:

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

const withNavigate = Component => props => {
const navigate = useNavigate();
return <Component {...props} navigate={navigate} />;
};

Decorate the component in your snippet with this withNavigate HOC.

export withNavigate(MyComponent);

Access the navigate function from props.

render(){
const { navigate } = this.props;

return (
<div>
<Routes>
<Route
path="/"
element={(
<div>
<Title title={'Arijit - Photowall'}/>
<Photowall posts={this.state.posts} onRemovePhoto={this.removePhoto} />
</div>
)}
/>
<Route
path="/addPhotos"
element={(
<AddPhoto
onAddPhoto={(addedPost) => {
this.addPhoto(addedPost);
navigate("/");
}}
/>
)}
/>
</Routes>
</div>
);
}

Using Typescript

interface WithRouter {
location: ReturnType<typeof useLocation>;
navigate: ReturnType<typeof useNavigate>;
params: ReturnType<typeof useParams>;
}

const withRouter = <P extends {}>(Component: React.ComponentType<P>) => (
props: Omit<P, keyof WithRouter>
) => {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();

return <Component {...(props as P)} {...{ location, navigate, params }} />;
};

Example Usage:

interface MyComponentProps {
foo: string;
}

type MyComponentPropsWithRouter = MyComponentProps & WithRouter

class MyComponent extends React.Component<MyComponentPropsWithRouter> {
render() {
const { foo, navigate, location, params } = this.props;
const { bar } = params as { bar?: string };

return (
<>
<h1>MyComponent: {location.pathname}</h1>
<h2>Foo prop: {foo}</h2>
<h2>Param?: {bar}</h2>
<button type="button" onClick={() => navigate("/test")}>
Navigate
</button>
</>
);
}
}

const MyDecoratedComponent = withRouter(MyComponent);

I have a problem with React Router Dom V6 and useState

react-router-dom is optimized to not unnecessarily unmount and remount components.

<Route path='user/:id' element={<UserForm />} />
<Route path='user' element={<UserForm />} />

When switching between "/user" and "/user/:id" the UserForm component will only get rerendered. The same is true if you navigate from one "/user/:id" path to a different "/user/:id" path. It's clear that UserForm has a dependency on this id route path parameter. You've a couple options for handling this.

  1. Use a useEffect hook with a dependency on the id route param and rerun/reset any state.

    const UserForm = () => {
    const navigate = useNavigate();
    const { id } = useParams<FormParams>();
    // const [searchParams, setSearchParams] = useSearchParams();
    const [loading, setLoading] = useState<boolean>(true);
    const [message, setMessage] = useState<MessageProps>();
    const [data, setData] = useState<UserModel>(newData);

    useEffect(() => {
    // reset state or re-issue side-effect based on id param
    }, [id]);
    ...
    }
  2. Add a React key to the UserForm component in the route so React will unmount/mount a new instance when switching between "/user" and "/user/:id". Note however that you'd still need a useEffect hook to rerun/reset logic/state when staying on the same route and only the id changes.

    <Route path='user/:id' element={<UserForm key="root" />} />
    <Route path='user' element={<UserForm key="id" />} />


Related Topics



Leave a reply



Submit