47

am trying to migrate the old react router dom code to v6 and I want to know how to listen for route change, I am now using useHistory

const history = useHistory()
//then
history.listen(...)

I did read the new docs and I did find that useHistory was changed to useNavigate

const navigate = useNavigate()
//then
navigate.listen(...) // listen is not a function

can you please help me find a way to listen to the route change in v6

// This is a React Router v6 app
import { useNavigate } from "react-router-dom";

function App() {
  let navigate = useNavigate();
  function handleClick() {
    navigate("/home");
  }
  return (
    <div>
      <button onClick={handleClick}>go home</button>
    </div>
  );
}
scripter
  • 1,211
  • 2
  • 11
  • 21
  • 1
    Does this answer your question? [React router v6 history.listen](https://stackoverflow.com/questions/68782781/react-router-v6-history-listen) – yuval.bl Jun 15 '22 at 15:57

4 Answers4

29

From documentation (https://reactrouter.com/en/main/hooks/use-location), use this hook

let location = useLocation();

React.useEffect(() => {
  ga('send', 'pageview');
}, [location]);
José Lozano Hernández
  • 1,813
  • 1
  • 17
  • 21
  • useLayoutEffect also would be more useful in some situation, to avoid interface rerendering/flickering – Griha Mikhailov Jun 09 '22 at 16:36
  • 1
    This solution is fine if the order of events doesn't matter, but useEffect()'s behaviour is to run child effects before parent effects (https://github.com/facebook/react/issues/15281). If useEffect is called high in the hierarchy, its effect will occur after all its children have rendered. – Mattias Martens Jul 26 '22 at 21:31
  • 1
    Edit queue is full, sigh. Working link: https://reactrouter.com/en/main/hooks/use-location – Leland Sep 15 '22 at 20:52
  • Thank you @Leland I've updated the link – José Lozano Hernández Sep 16 '22 at 07:46
22

The navigate function is a function, not an object like the older react-router-dom version 5's history object.

You can still create a custom history object but you'll need to create a custom router to use it. This allows you to import your history object and create listeners.

Create a custom router example, use one of the higher-level routers as an example for how they manage the location and state, i.e. BrowserRouter:

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

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

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

In your code create the custom history object for use by your new custom router and other components. Ensure you have history@5 installed as a project dependency. This is the same version used by RRDv6. If you need to install it run npm i history@5 to add it to the project's dependencies.

const history = createBrowserHistory();
export default history;

Use your router and pass your history object to it.

import CustomRouter from '../CustomRouter';
import history from '../myHistory';

...

<CustomRouter history={history}>
  ....
</CustomRouter>

In a component you want to listen to location changes on, import your history object and invoke the listen callback as you did previously.

import history from '../myHistory';

...

useEffect(() => {
  const unlisten = history.listen((location, action) => {
    // ... logic
  });

  return unlisten;
}, []);

If you want, you may be able to also create your own custom useHistory hook that simply returns your history object.

Update

react-router-dom has started exporting a HistoryRouter for a use case like this. Instead of importing the low-level Router and implementing the internal logic you import unstable_HistoryRouter as HistoryRouter and pass your custom history object (memory, hash, etc).

import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom";
import history from "../myHistory";

...

<HistoryRouter history={history}>
  ....
</HistoryRouter>

Notes on RRDv6.4+

If you are using RRDv6.4+ and not using the Data routers the good-ish news is that unstable_HistoryRouter is still being exported through at least RRDv6.8.0. You can follow along the filed issue in the repo here.

If you are using the Data routers then the new "unstable" method is to use an attached navigate function from the router object directly.

Example:

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

// If you need to navigate externally, instead of history.push you can do:
router.navigate('/path');

// And instead of history.replace you can do:
router.navigate('/path', { replace: true });

// And instead of history.listen you can:
router.subscribe((state) => console.log('new state', state));

I've had mixed results with using the history.listen solution between versions 6.4 and 6.8, so probably best to keep an eye on the linked issue for whatever the RRD maintainers say is the current "unstable" method of accessing the "history".

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • 4
    Wouldn't it be easier to just use `useEffect` with the `location.pathname` in de dependency array? I.e. ` useEffect(() => { // ...code }, [location.pathname])` – Reinier68 Mar 24 '22 at 10:46
  • @Reinier68 It depends on where you want to listen... if you are in a React component and you can simply use the `useLocation` hook with an `useEffect` hook then perhaps that is sufficient. If you need to instantiate a single history object to attach a more substantial listener to, to be used outside react, like in a network service module, this approach works. My answer here is only a conversion to RRDv6 that allows one to use the `history.listen` logic. – Drew Reese Mar 24 '22 at 16:31
  • For React Router v6.7, you need to import the ``createBrowserHistory`` function from the '@remix-run/router' package (not 'history') – SameOldNick Jan 24 '23 at 20:33
  • @SameOldNick I believe that is true only if you are using one of the Data routers. It's a good point though and I will try to update my answer to better reflect some of the more recent RRDv6.4+ changes. – Drew Reese Jan 24 '23 at 20:39
  • In react-router-dom v6.8.0 or even earlier, trying to attach a listener to the history, will throw an error: A history only accepts one active listener. Feel free to update your answer with the contents of mine below, if you agree. – Wu Wei Jan 31 '23 at 16:34
  • 1
    @anarchist912 The topic of this post is more general than just listening for back navigations. We're good though, I've updated my answer here with details for working with the newer versions of RRD. Thanks for the callout. – Drew Reese Jan 31 '23 at 18:11
11

To add to the accepted answer (can't comment, not enough rep points), subscribing to the history through a useEffect with location.pathname in the dependency array won't work if the navigation unmounts the component you're attempting to call the useEffect from.

hiroshin
  • 111
  • 2
  • 5
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 18 '22 at 08:07
4

If you need to react to a change in the route due to back button specifically:

In react-router-dom v6.8.0 or even earlier, trying to attach a listener to the history, will throw an error: A history only accepts one active listener.

I learnt that react-router-dom seems to introduce a lot of changes between the minor versions as well, so you should take words like unsafe and unstable , like in unstable_HistoryRouter especially serious. They will break sooner or later, if you're not very lucky.

In my case I had to upgrade to get the reintroduced optional route params, and the UNSAFE_NavigationContext my former colleague decided to use, didn't work anymore.

So here's a high level approach, that allows you to listen to the actions on the Router's history stack, without attaching another listener to the router yourself. Which is fine, as it already has one listener by default, and it's just not exposed, but the actions derived from it are, which is enough.

In the following example we are reacting to changes in location and for each change, we check if it was due to a POP action, thats e.g. triggered when the browser's back button is used, and then execute whatever..

import { useEffect } from "react";
import {
  Location,
  NavigationType,
  useLocation,
  useNavigationType,
} from "react-router-dom";

export const useBackListener = (callback: () => void) => {
  const location: Location = useLocation();
  const navType: NavigationType = useNavigationType();

  useEffect(() => {
    if (navType === "POP" && location.key !== "default") {
      if (someCondition === true) callback();
      else {
        doSomethingElse();
      }
    }
  }, [location]);
};
Wu Wei
  • 1,827
  • 1
  • 15
  • 27