Detect Route Change with react-router
Asked Answered
L

10

203

I have to implement some business logic depending on browsing history.

What I want to do is something like this:

reactRouter.onUrlChange(url => {
   this.history.push(url);
});

Is there any way to receive a callback from react-router when the URL gets updated?

Leann answered 28/7, 2017 at 12:36 Comment(1)
What version of react-router are you using? That will determine the best approach. I will provide an answer once you update. That being said, the withRouter HoC is probably your best bet for making a component location aware. It will update your component with new ({match, history, and location }) anytime a route changes. This way you don't need to manually subscribe and unsubscribe to events. Meaning it is easy to use with functional stateless components as well as class components.Fallen
W
182

You can make use of history.listen() function when trying to detect the route change. Considering you are using react-router v4, wrap your component with withRouter HOC to get access to the history prop.

history.listen() returns an unlisten function. You'd use this to unregister from listening.

You can configure your routes like

index.js

ReactDOM.render(
      <BrowserRouter>
            <AppContainer>
                   <Route exact path="/" Component={...} />
                   <Route exact path="/Home" Component={...} />
           </AppContainer>
        </BrowserRouter>,
  document.getElementById('root')
);

and then in AppContainer.js

class App extends Component {
  
  componentWillMount() {
    this.unlisten = this.props.history.listen((location, action) => {
      console.log("on route change");
    });
  }
  componentWillUnmount() {
      this.unlisten();
  }
  render() {
     return (
         <div>{this.props.children}</div>
      );
  }
}
export default withRouter(App);

From the history docs:

You can listen for changes to the current location using history.listen:

history.listen((location, action) => {
      console.log(`The current URL is ${location.pathname}${location.search}${location.hash}`)
  console.log(`The last navigation action was ${action}`)
})

The location object implements a subset of the window.location interface, including:

**location.pathname** - The path of the URL
**location.search** - The URL query string
**location.hash** - The URL hash fragment

Locations may also have the following properties:

location.state - Some extra state for this location that does not reside in the URL (supported in createBrowserHistory and createMemoryHistory)

location.key - A unique string representing this location (supported in createBrowserHistory and createMemoryHistory)

The action is one of PUSH, REPLACE, or POP depending on how the user got to the current URL.

When you are using react-router v3 you can make use of history.listen() from history package as mentioned above or you can also make use browserHistory.listen()

You can configure and use your routes like

import {browserHistory} from 'react-router';

class App extends React.Component {

    componentDidMount() {
          this.unlisten = browserHistory.listen( location =>  {
                console.log('route changes');
                
           });
      
    }
    componentWillUnmount() {
        this.unlisten();
     
    }
    render() {
        return (
               <Route path="/" onChange={yourHandler} component={AppContainer}>
                   <IndexRoute component={StaticContainer}  />
                   <Route path="/a" component={ContainerA}  />
                   <Route path="/b" component={ContainerB}  />
            </Route>
        )
    }
} 
Willianwillie answered 28/7, 2017 at 12:45 Comment(19)
he's using v3 and the second sentence of your answer says "Considering you are using react-router v4"Fallen
@KyleRichardson I think you misunderstood me again, I certainly have to work on my english. I meant that if you are using react-router v4 and you are using history object then you need to wrap your component with withRouterWillianwillie
@KyleRichardson I you see my complete answer, I have added ways to do it in v3 also. One more thing, the OP commented that he is using v3 today and I had answerd on the question yesterdayWillianwillie
@ShubhamKhatri Yes but the way your answer reads is wrong. He is not using v4... Also, why would you use history.listen() when using withRouter already updates your component with new props every time routing occurs? You could do a simple comparison of nextProps.location.href === this.props.location.href in componentWillUpdate to perform anything you need to do if it has changed.Fallen
@KyleRichardson as I said OP mentioned that he is using v3 today and there are multiple ways to do it, 1. make use of history from withRouter 2. make use of history from history package 3. or do a props comparison. However I think do check is compentWillUpdate is not the best solution as it is fired everytime the component needs to updateWillianwillie
@ShubhamKhatri ok :) Sorry if I find your answer hard to follow. It looks like you're suggesting to use withRouter AND history.listen().Fallen
@KyleRichardson, I would say, if you are using react-router v4 , follow the first example using history object which is available as prop after you wrap your component with withRouter or use history.listen() or browserHistory.listen() in case of react-router v3Willianwillie
@ShubhamKhatri I would use withRouter and do props comparison instead of history.listen().Fallen
@KyleRichardson as I said there are multiple ways to do it, I prefer history.listen and it is also suggessted on the github issue github.com/ReactTraining/react-router/issues/3554Willianwillie
I used react router v4 with React Router Config. I'm not able to use withRouter to wrap my App component. It raise this error:Uncaught Error: You should not use <Route> or withRouter() outside a <Router>. My app component render logic : return <BrowserRouter>{renderRoutes(routes)}</BrowserRouter>;Subclimax
@ShubhamKhatri When I change the route using history.push, the listen doesn't fire, any idea? btw when route changes via Link, everything works fine.Duress
@MehdiDehghani If could be possible that you are using custom history in Router and using push method custom history object whereas you listen to history from props. If thats not the case, could you please provide more details, possibly in a new postWillianwillie
@ShubhamKhatri What you mean by "you are using custom history in Router"? I passed the props to the child component, then in componentDidMount add the listener, and unlisten it in the componentWillUnmount, and after some ajax req, this.props.params.history.push('/url');, the listen doesn't fire in this caseDuress
@MehdiDehghani Why do you read history from params, shouldn't it be this.props.history.push('/url')Willianwillie
@ShubhamKhatri good question, because of redux and its mapStateToProps and mapDispatchToProps I only have mapped items in the props object. the whole part in my prev comment happens in child component btw. I passed params via its parent (which is stateless component), e.g <MyChildCom params={props} />Duress
@ShubhamKhatri I just tried to use withRouter and this.props.history.push('/url'), still listen doesn't fire.Duress
@ShubhamKhatri Will it work if I change the url manually in the browser and press enter?Readymade
When I change URL by hand history.listen does not fire also. And I need this.Diannediannne
Not working for me, I don't know why. The debugger never hit the event line.Herrod
G
186

Update for React Router 5.1+.

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function SomeComponent() {
  const location = useLocation();

  useEffect(() => {
    console.log('Location changed');
  }, [location]);

  ...
}
Graupel answered 11/4, 2020 at 6:57 Comment(4)
when you try to detect change on not changing route this solution works perfect!Selfpossessed
As the user asks, If you try to do history.push() in the useEffect you will get an infinite loop, so i don't see how this is a solution.Barrister
@Cristian Muscalu, try to replace location with location.pathname in dependency array.Cumbersome
This doesn't work for me - it only works when I change url search parameter inside a specific oute, but doesn't work when user click on <Link> to be redirected to different page.Aceves
W
182

You can make use of history.listen() function when trying to detect the route change. Considering you are using react-router v4, wrap your component with withRouter HOC to get access to the history prop.

history.listen() returns an unlisten function. You'd use this to unregister from listening.

You can configure your routes like

index.js

ReactDOM.render(
      <BrowserRouter>
            <AppContainer>
                   <Route exact path="/" Component={...} />
                   <Route exact path="/Home" Component={...} />
           </AppContainer>
        </BrowserRouter>,
  document.getElementById('root')
);

and then in AppContainer.js

class App extends Component {
  
  componentWillMount() {
    this.unlisten = this.props.history.listen((location, action) => {
      console.log("on route change");
    });
  }
  componentWillUnmount() {
      this.unlisten();
  }
  render() {
     return (
         <div>{this.props.children}</div>
      );
  }
}
export default withRouter(App);

From the history docs:

You can listen for changes to the current location using history.listen:

history.listen((location, action) => {
      console.log(`The current URL is ${location.pathname}${location.search}${location.hash}`)
  console.log(`The last navigation action was ${action}`)
})

The location object implements a subset of the window.location interface, including:

**location.pathname** - The path of the URL
**location.search** - The URL query string
**location.hash** - The URL hash fragment

Locations may also have the following properties:

location.state - Some extra state for this location that does not reside in the URL (supported in createBrowserHistory and createMemoryHistory)

location.key - A unique string representing this location (supported in createBrowserHistory and createMemoryHistory)

The action is one of PUSH, REPLACE, or POP depending on how the user got to the current URL.

When you are using react-router v3 you can make use of history.listen() from history package as mentioned above or you can also make use browserHistory.listen()

You can configure and use your routes like

import {browserHistory} from 'react-router';

class App extends React.Component {

    componentDidMount() {
          this.unlisten = browserHistory.listen( location =>  {
                console.log('route changes');
                
           });
      
    }
    componentWillUnmount() {
        this.unlisten();
     
    }
    render() {
        return (
               <Route path="/" onChange={yourHandler} component={AppContainer}>
                   <IndexRoute component={StaticContainer}  />
                   <Route path="/a" component={ContainerA}  />
                   <Route path="/b" component={ContainerB}  />
            </Route>
        )
    }
} 
Willianwillie answered 28/7, 2017 at 12:45 Comment(19)
he's using v3 and the second sentence of your answer says "Considering you are using react-router v4"Fallen
@KyleRichardson I think you misunderstood me again, I certainly have to work on my english. I meant that if you are using react-router v4 and you are using history object then you need to wrap your component with withRouterWillianwillie
@KyleRichardson I you see my complete answer, I have added ways to do it in v3 also. One more thing, the OP commented that he is using v3 today and I had answerd on the question yesterdayWillianwillie
@ShubhamKhatri Yes but the way your answer reads is wrong. He is not using v4... Also, why would you use history.listen() when using withRouter already updates your component with new props every time routing occurs? You could do a simple comparison of nextProps.location.href === this.props.location.href in componentWillUpdate to perform anything you need to do if it has changed.Fallen
@KyleRichardson as I said OP mentioned that he is using v3 today and there are multiple ways to do it, 1. make use of history from withRouter 2. make use of history from history package 3. or do a props comparison. However I think do check is compentWillUpdate is not the best solution as it is fired everytime the component needs to updateWillianwillie
@ShubhamKhatri ok :) Sorry if I find your answer hard to follow. It looks like you're suggesting to use withRouter AND history.listen().Fallen
@KyleRichardson, I would say, if you are using react-router v4 , follow the first example using history object which is available as prop after you wrap your component with withRouter or use history.listen() or browserHistory.listen() in case of react-router v3Willianwillie
@ShubhamKhatri I would use withRouter and do props comparison instead of history.listen().Fallen
@KyleRichardson as I said there are multiple ways to do it, I prefer history.listen and it is also suggessted on the github issue github.com/ReactTraining/react-router/issues/3554Willianwillie
I used react router v4 with React Router Config. I'm not able to use withRouter to wrap my App component. It raise this error:Uncaught Error: You should not use <Route> or withRouter() outside a <Router>. My app component render logic : return <BrowserRouter>{renderRoutes(routes)}</BrowserRouter>;Subclimax
@ShubhamKhatri When I change the route using history.push, the listen doesn't fire, any idea? btw when route changes via Link, everything works fine.Duress
@MehdiDehghani If could be possible that you are using custom history in Router and using push method custom history object whereas you listen to history from props. If thats not the case, could you please provide more details, possibly in a new postWillianwillie
@ShubhamKhatri What you mean by "you are using custom history in Router"? I passed the props to the child component, then in componentDidMount add the listener, and unlisten it in the componentWillUnmount, and after some ajax req, this.props.params.history.push('/url');, the listen doesn't fire in this caseDuress
@MehdiDehghani Why do you read history from params, shouldn't it be this.props.history.push('/url')Willianwillie
@ShubhamKhatri good question, because of redux and its mapStateToProps and mapDispatchToProps I only have mapped items in the props object. the whole part in my prev comment happens in child component btw. I passed params via its parent (which is stateless component), e.g <MyChildCom params={props} />Duress
@ShubhamKhatri I just tried to use withRouter and this.props.history.push('/url'), still listen doesn't fire.Duress
@ShubhamKhatri Will it work if I change the url manually in the browser and press enter?Readymade
When I change URL by hand history.listen does not fire also. And I need this.Diannediannne
Not working for me, I don't know why. The debugger never hit the event line.Herrod
F
133

react-router v6

In react-router v6, this can be done by combining the useLocation and useEffect hooks

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

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

  React.useEffect(() => {
    // runs on location, i.e. route, change
    console.log('handle route change here', location)
  }, [location])
  ...
}

For convenient reuse, you can do this in a custom useLocationChange hook

// runs action(location) on location, i.e. route, change
const useLocationChange = (action) => {
  const location = useLocation()
  React.useEffect(() => { action(location) }, [location])
}

const MyComponent1 = () => {
  useLocationChange((location) => { 
    console.log('handle route change here', location) 
  })
  ...
}

const MyComponent2 = () => {
  useLocationChange((location) => { 
    console.log('and also here', location) 
  })
  ...
}

If you also need to see the previous route on change, you can combine with a usePrevious hook

const usePrevious = (value) => {
  const ref = React.useRef()
  React.useEffect(() => { ref.current = value })

  return ref.current
}

const useLocationChange = (action) => {
  const location = useLocation()
  const prevLocation = usePrevious(location)
  React.useEffect(() => { 
    action(location, prevLocation) 
  }, [location])
}

const MyComponent1 = () => {
  useLocationChange((location, prevLocation) => { 
    console.log('changed from', prevLocation, 'to', location) 
  })
  ...
}

It's important to note that all the above fire on the first client route being mounted, as well as subsequent changes. If that's a problem, use the latter example and check that a prevLocation exists before doing anything.

Foment answered 15/6, 2020 at 13:27 Comment(7)
I have a question. If several components have been rendered and they are all watching useLocation then all their useEffects will be triggered. How do I verify that this location is correct for the specific component that will be displayed?Hayner
Hey @Hayner - just to clarify location here is the browser location, so it's the same in every component and always correct in that sense. If you use the hook in different components they'll all receive the same values when location changes. I guess what they do with that info will be different, but it's always consistent.Foment
That makes sense. Just wondering how a component will know if the location change is relevant to itself performing an action. Eg a component receives dashboard/list but how does it know if it’s tied to that location or not?Hayner
Unless I do something like if (location.pathName === “dashboard/list”) { ..... actions }. It doesn’t seem very elegant hardcoding path to a component though.Hayner
Using the useLocation/useEffect method helped get past the "no change after first load" issue on single-page Ionic 5 React projects (using useParams). None of the Ionic Lifecycle methods were triggered after first load.Frink
How to do this same useLocationChange but with TS? and also react is complaining React Hook useEffect has a missing dependency: 'action'. Either include it or remove the dependency array. If 'action' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-depsRhinestone
I love this answer, last snippet is very useful. However, as @RubenMarcus said there is a warning. How to make it work without the warning?Collocation
E
19

If you want to listen to the history object globally, you'll have to create it yourself and pass it to the Router. Then you can listen to it with its listen() method:

// Use Router from react-router, not BrowserRouter.
import { Router } from 'react-router';

// Create history object.
import createHistory from 'history/createBrowserHistory';
const history = createHistory();

// Listen to history changes.
// You can unlisten by calling the constant (`unlisten()`).
const unlisten = history.listen((location, action) => {
  console.log(action, location.pathname, location.state);
});

// Pass history to Router.
<Router history={history}>
   ...
</Router>

Even better if you create the history object as a module, so you can easily import it anywhere you may need it (e.g. import history from './history';

Escobar answered 28/7, 2017 at 12:49 Comment(1)
When must one call unlisten()? When the whole app unmounts?Yovonnda
L
13

This is an old question and I don't quite understand the business need of listening for route changes to push a route change; seems roundabout.

BUT if you ended up here because all you wanted was to update the 'page_path' on a react-router route change for google analytics / global site tag / something similar, here's a hook you can now use. I wrote it based on the accepted answer:

useTracking.js

import { useEffect } from 'react'
import { useHistory } from 'react-router-dom'

export const useTracking = (trackingId) => {
  const { listen } = useHistory()

  useEffect(() => {
    const unlisten = listen((location) => {
      // if you pasted the google snippet on your index.html
      // you've declared this function in the global
      if (!window.gtag) return

      window.gtag('config', trackingId, { page_path: location.pathname })
    })

    // remember, hooks that add listeners
    // should have cleanup to remove them
    return unlisten
  }, [trackingId, listen])
}

You should use this hook once in your app, somewhere near the top but still inside a router. I have it on an App.js that looks like this:

App.js

import * as React from 'react'
import { BrowserRouter, Route, Switch } from 'react-router-dom'

import Home from './Home/Home'
import About from './About/About'
// this is the file above
import { useTracking } from './useTracking'

export const App = () => {
  useTracking('UA-USE-YOURS-HERE')

  return (
    <Switch>
      <Route path="/about">
        <About />
      </Route>
      <Route path="/">
        <Home />
      </Route>
    </Switch>
  )
}

// I find it handy to have a named export of the App
// and then the default export which wraps it with
// all the providers I need.
// Mostly for testing purposes, but in this case,
// it allows us to use the hook above,
// since you may only use it when inside a Router
export default () => (
  <BrowserRouter>
    <App />
  </BrowserRouter>
)
Liebermann answered 1/3, 2020 at 8:50 Comment(1)
i have tried your code but it can not detect when i change a route. it works when i refresh the page. but when is change route the useTracking() is not called again in app.js, is there a way i can make useTracking() get called again when a route changes ?Acropetal
T
2

Use the useLocation() Hook to detect the URL change and put it in dependency array in useEffect() this trick worked for me

const App = () => {

  const location = useLocation();

  useEffect(() => {
    window.scroll(0,0);
  }, [location]);

  return (
    <React.Fragment>
      <Routes>
        <Route path={"/"} element={<Template/>} >
          <Route index={true} element={<Home/>} />
          <Route path={"cart"} element={<Cart/>} />
          <Route path={"signin"} element={<Signin/>} />
          <Route path={"signup"} element={<Signup/>} />
          <Route path={"product/:slug"} element={<Product/>} />
          <Route path={"category/:category"} element={<ProductList/>} />
        </Route>
      </Routes>
    </React.Fragment>
  );
}

export default App;
Turtledove answered 25/10, 2022 at 18:46 Comment(0)
P
1

I came across this question as I was attempting to focus the ChromeVox screen reader to the top of the "screen" after navigating to a new screen in a React single page app. Basically trying to emulate what would happen if this page was loaded by following a link to a new server-rendered web page.

This solution doesn't require any listeners, it uses withRouter() and the componentDidUpdate() lifecycle method to trigger a click to focus ChromeVox on the desired element when navigating to a new url path.


Implementation

I created a "Screen" component which is wrapped around the react-router switch tag which contains all the apps screens.

<Screen>
  <Switch>
    ... add <Route> for each screen here...
  </Switch>
</Screen>

Screen.tsx Component

Note: This component uses React + TypeScript

import React from 'react'
import { RouteComponentProps, withRouter } from 'react-router'

class Screen extends React.Component<RouteComponentProps> {
  public screen = React.createRef<HTMLDivElement>()
  public componentDidUpdate = (prevProps: RouteComponentProps) => {
    if (this.props.location.pathname !== prevProps.location.pathname) {
      // Hack: setTimeout delays click until end of current
      // event loop to ensure new screen has mounted.
      window.setTimeout(() => {
        this.screen.current!.click()
      }, 0)
    }
  }
  public render() {
    return <div ref={this.screen}>{this.props.children}</div>
  }
}

export default withRouter(Screen)

I had tried using focus() instead of click(), but click causes ChromeVox to stop reading whatever it is currently reading and start again where I tell it to start.

Advanced note: In this solution, the navigation <nav> which inside the Screen component and rendered after the <main> content is visually positioned above the main using css order: -1;. So in pseudo code:

<Screen style={{ display: 'flex' }}>
  <main>
  <nav style={{ order: -1 }}>
<Screen>

If you have any thoughts, comments, or tips about this solution, please add a comment.

Pentheas answered 19/3, 2019 at 21:8 Comment(0)
F
1
import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import Sidebar from './Sidebar';
import Chat from './Chat';

<Router>
    <Sidebar />
        <Switch>
            <Route path="/rooms/:roomId" component={Chat}>
            </Route>
        </Switch>
</Router>

import { useHistory } from 'react-router-dom';
function SidebarChat(props) {
    **const history = useHistory();**
    var openChat = function (id) {
        **//To navigate**
        history.push("/rooms/" + id);
    }
}

**//To Detect the navigation change or param change**
import { useParams } from 'react-router-dom';
function Chat(props) {
    var { roomId } = useParams();
    var roomId = props.match.params.roomId;

    useEffect(() => {
       //Detect the paramter change
    }, [roomId])

    useEffect(() => {
       //Detect the location/url change
    }, [location])
}
Folio answered 17/9, 2020 at 10:40 Comment(0)
R
1

You can use the useLocation with componentDidUpdate for getting the route change for class component and useEffect for functional component

In Class component

import { useLocation } from "react-router";

class MainApp extends React.Component {
    constructor(props) {
       super(props);
    }
    async componentDidUpdate(prevProps) {
       if(this.props.location.pathname !== prevProps.location.pathname)
       {
         //route has been changed. do something here
       } 
    }
}


function App() {
 const location = useLocation()
 return <MainApp location={location} />
}

In functional component

function App() {
     const location = useLocation()
     useEffect(() => {
        //route change detected. do something here
     }, [location]) //add location in dependency. It detects the location change
     return <Routes> 
              <Route path={"/"} element={<Home/>} >
              <Route path={"login"} element={<Login/>} />
            </Routes>
}
Reveal answered 4/1, 2023 at 6:57 Comment(0)
B
-1

React Router V5

If you want the pathName as a string ('/' or 'users'), you can use the following:

  // React Hooks: React Router DOM
  let history = useHistory();
  const location = useLocation();
  const pathName = location.pathname;
Baltimore answered 29/5, 2020 at 7:22 Comment(2)
How does this apply to detecting route changes?Brana
pathname = window.location.pathnameBunin

© 2022 - 2024 — McMap. All rights reserved.