171

I have a couple of buttons that acts as routes. Everytime the route is changed, I want to make sure the button that is active changes.

Is there a way to listen to route changes in react router v4?

Kasper
  • 8,678
  • 10
  • 34
  • 53

13 Answers13

189

I use withRouter to get the location prop. When the component is updated because of a new route, I check if the value changed:

@withRouter
class App extends React.Component {

  static propTypes = {
    location: React.PropTypes.object.isRequired
  }

  // ...

  componentDidUpdate(prevProps) {
    if (this.props.location !== prevProps.location) {
      this.onRouteChanged();
    }
  }

  onRouteChanged() {
    console.log("ROUTE CHANGED");
  }

  // ...
  render(){
    return <Switch>
        <Route path="/" exact component={HomePage} />
        <Route path="/checkout" component={CheckoutPage} />
        <Route path="/success" component={SuccessPage} />
        // ...
        <Route component={NotFound} />
      </Switch>
  }
}

Hope it helps

brickpop
  • 2,576
  • 1
  • 11
  • 12
  • 23
    Use 'this.props.location.pathname' in react router v4. – ptorsson Oct 25 '17 at 17:13
  • 5
    @ledfusion I am doing the same and using `withRouter`, but I am getting error `You should not use or withRouter() outside a `. I don't see any `` component in above code. So how is it working? – darKnight May 30 '18 at 10:18
  • 1
    Hi @maverick. I'm not sure how your code looks like, but in the example above, the `` component is acting as the de-facto router. Only the first `` entry to have a matching path, will be rendered. There is no need of any `` component in this scenario – brickpop Jun 05 '18 at 09:58
  • 1
    to use @withRouter you need to install npm install --save-dev transform-decorators-legacy – Sigex Sep 22 '18 at 17:47
  • how exactly is withRouter. – Jovylle Bermudez Oct 25 '20 at 14:31
  • I was going crazy checking if this was a problem with `history.push`, but the problem was visiting the same route and `componentDidUpdate` came to the rescue. – eLRuLL Feb 10 '21 at 22:20
  • Sometimes we are so dependent on API's we forget the basics like this solution, thanks man! – mukuljainx Feb 15 '21 at 12:52
87

To expand on the above, you will need to get at the history object. If you are using BrowserRouter, you can import withRouter and wrap your component with a higher-order component (HoC) in order to have access via props to the history object's properties and functions.

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

const myComponent = ({ history }) => {

    history.listen((location, action) => {
        // location is an object like window.location
        console.log(action, location.pathname, location.state)
    });

    return <div>...</div>;
};

export default withRouter(myComponent);

The only thing to be aware of is that withRouter and most other ways to access the history seem to pollute the props as they de-structure the object into it.

Kaloyan Kosev
  • 10,232
  • 4
  • 48
  • 82
Sam Parmenter
  • 1,603
  • 1
  • 10
  • 16
  • 1
    The answer helped me to understand something regardless of the question:). But fix `withRoutes` to `withRouter`. – Sergey Reutskiy Feb 10 '17 at 21:14
  • 1
    Yep, sorry, thanks for pointing that out. I've amended the post. I put the correct import at the top of the question and then mis-spelt it in the code example. – Sam Parmenter Feb 12 '17 at 10:32
  • 5
    I think the [current version of withRouter](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/withRouter.md) passes `history` rather than the variable `listen`. – mikebridge Apr 24 '17 at 18:18
  • 6
    It would be good to amend the post to demonstrate unlistening; this code has a memory leak in it. – AndrewSouthpaw Jun 09 '19 at 05:10
  • you should subscribe only once! Now you will subscribe on each component re render. – Ievgen Naida Aug 06 '20 at 09:59
63

v5.1 introduces the useful hook useLocation

https://reacttraining.com/blog/react-router-v5-1/#uselocation

import { Switch, useLocation } from 'react-router-dom'

function usePageViews() {
  let location = useLocation()

  useEffect(
    () => {
      ga.send(['pageview', location.pathname])
    },
    [location]
  )
}

function App() {
  usePageViews()
  return <Switch>{/* your routes here */}</Switch>
}
Mugen
  • 5,490
  • 3
  • 47
  • 97
  • 18
    Just a note as I was having trouble with an error: `Cannot read property 'location' of undefined at useLocation`. You need to make sure that the useLocation() call is not in the same component that puts the router into the tree: [see here](https://github.com/ReactTraining/react-router/issues/7015) – toddg Apr 09 '20 at 21:18
  • this does not trigger the action on nested routes unfortunately – Eugene Kuzmenko Sep 30 '20 at 15:22
  • Thanks for your awesome answer. – AmerllicA Feb 10 '21 at 13:23
  • This is the neatest and shortest solution. – Waleed93 Apr 30 '21 at 00:05
34

You should to use history v4 lib.

Example from there

history.listen((location, action) => {
  console.log(`The current URL is ${location.pathname}${location.search}${location.hash}`)
  console.log(`The last navigation action was ${action}`)
})
Sergey Reutskiy
  • 2,501
  • 2
  • 16
  • 13
  • 2
    The history.pushState() and history.replaceState() calls don't trigger the popstate event, so this alone won't cover all route changes. – Ryan Feb 10 '17 at 06:20
  • 1
    @Ryan It seems that `history.push` does trigger `history.listen`. See the **Using a Base URL** example in [history v4 docs](https://github.com/ReactTraining/history/blob/master/README.md). Because that `history` is actually a wrapper of the native `history` object of a browser, it doesn’t behave exactly like the native one. – Rockallite May 05 '18 at 09:27
  • This feels like a better solution, as often times you need to listen to route changes for event pushing, which is unrelated to react component lifecycle events. – Daniel Dubovski Jul 25 '18 at 08:14
  • 13
    Potential memory leak! Very important that you do this! "When you attach a listener using history.listen, it returns a function that can be used to remove the listener, which can then be invoked in cleanup logic: `const unlisten = history.listen(myListener); unlisten();` – Dehan Dec 04 '18 at 12:31
  • Go here for documentation on history package. https://github.com/ReactTraining/history/blob/master/docs/GettingStarted.md – Jason Kim Dec 20 '19 at 07:32
  • @DehandeCroos Is there still a memory leak if this is used in a `useEffect` call? – Carl Edwards Jan 01 '20 at 04:42
  • Yes!! You need to unlisten inside an effect. An effect can return a function, React will run it when it is time to clean up. You should unlisten inside this. https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1 – Dehan Jan 01 '20 at 06:41
30

withRouter, history.listen, and useEffect (React Hooks) works quite nicely together:

import React, { useEffect } from 'react'
import { withRouter } from 'react-router-dom'

const Component = ({ history }) => {
    useEffect(() => history.listen(() => {
        // do something on route change
        // for my example, close a drawer
    }), [])

    //...
}

export default withRouter(Component)

The listener callback will fire any time a route is changed, and the return for history.listen is a shutdown handler that plays nicely with useEffect.

noetix
  • 4,140
  • 2
  • 22
  • 43
17
import React, { useEffect } from 'react';
import { useLocation } from 'react-router';

function MyApp() {

  const location = useLocation();

  useEffect(() => {
      console.log('route has been changed');
      ...your code
  },[location.pathname]);

}

with hooks

Ahmed Boutaraa
  • 663
  • 7
  • 7
  • Holly Jesys! how does it work? Your answer is cool! buti put debugger point in *useEffect* but any time i changed the pathname the location stayed **undefined**? can you share any good article? becuase it's hard to find any clear information – Alexey Nikonov Mar 29 '20 at 20:03
  • 1
    Use `react-router-dom` instead of `react-router` – Nolesh Sep 18 '20 at 04:46
12

With hooks:

import { useEffect } from 'react'
import { withRouter } from 'react-router-dom'
import { history as historyShape } from 'react-router-prop-types'

const DebugHistory = ({ history }) => {
  useEffect(() => {
    console.log('> Router', history.action, history.location])
  }, [history.location.key])

  return null
}

DebugHistory.propTypes = { history: historyShape }

export default withRouter(DebugHistory)

Import and render as <DebugHistory> component

Tymek
  • 2,365
  • 1
  • 20
  • 40
8
import { useHistory } from 'react-router-dom';

const Scroll = () => {
  const history = useHistory();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [history.location.pathname]);

  return null;
}
weiya ou
  • 909
  • 8
  • 13
3

For functional components try useEffect with props.location.

import React, {useEffect} from 'react';

const SampleComponent = (props) => {

      useEffect(() => {
        console.log(props.location);
      }, [props.location]);

}

export default SampleComponent;
Dilshan Liyanage
  • 3,382
  • 1
  • 24
  • 27
2

With react Hooks, I am using useEffect

import React from 'react'
const history = useHistory()
const queryString = require('query-string')
const parsed = queryString.parse(location.search)
const [search, setSearch] = useState(parsed.search ? parsed.search : '')

useEffect(() => {
  const parsedSearch = parsed.search ? parsed.search : ''
  if (parsedSearch !== search) {
    // do some action! The route Changed!
  }
}, [location.search])

in this example, Im scrolling up when the route change:

import React from 'react'
import { useLocation } from 'react-router-dom'

const ScrollToTop = () => {
  const location = useLocation()

  React.useEffect(() => {
    window.scrollTo(0, 0)
  }, [location.key])

  return null
}

export default ScrollToTop
Alan
  • 4,576
  • 25
  • 43
1

In some cases you might use render attribute instead of component, in this way:

class App extends React.Component {

    constructor (props) {
        super(props);
    }

    onRouteChange (pageId) {
        console.log(pageId);
    }

    render () {
        return  <Switch>
                    <Route path="/" exact render={(props) => { 
                        this.onRouteChange('home');
                        return <HomePage {...props} />;
                    }} />
                    <Route path="/checkout" exact render={(props) => { 
                        this.onRouteChange('checkout');
                        return <CheckoutPage {...props} />;
                    }} />
                </Switch>
    }
}

Notice that if you change state in onRouteChange method, this could cause 'Maximum update depth exceeded' error.

Fernando
  • 714
  • 2
  • 6
  • 18
0

With the useEffect hook it's possible to detect route changes without adding a listener.

import React, { useEffect }           from 'react';
import { Switch, Route, withRouter }  from 'react-router-dom';
import Main                           from './Main';
import Blog                           from './Blog';


const App  = ({history}) => {

    useEffect( () => {

        // When route changes, history.location.pathname changes as well
        // And the code will execute after this line

    }, [history.location.pathname]);

    return (<Switch>
              <Route exact path = '/'     component = {Main}/>
              <Route exact path = '/blog' component = {Blog}/>
            </Switch>);

}

export default withRouter(App);

Erik Martín Jordán
  • 1,768
  • 1
  • 14
  • 21
0

I just dealt with this problem, so I'll add my solution as a supplement on other answers given.

The problem here is that useEffect doesn't really work as you would want it to, since the call only gets triggered after the first render so there is an unwanted delay.
If you use some state manager like redux, chances are that you will get a flicker on the screen because of lingering state in the store.

What you really want is to use useLayoutEffect since this gets triggered immediately.

So I wrote a small utility function that I put in the same directory as my router:

export const callApis = (fn, path) => {
    useLayoutEffect(() => {
      fn();
    }, [path]);
};

Which I call from within the component HOC like this:

callApis(() => getTopicById({topicId}), path);

path is the prop that gets passed in the match object when using withRouter.

I'm not really in favour of listening / unlistening manually on history. That's just imo.

html_programmer
  • 14,612
  • 12
  • 59
  • 125