Détecter le changement de route avec react-router

87

Je dois implémenter une logique métier en fonction de l'historique de navigation.

Ce que je veux faire est quelque chose comme ceci:

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

Existe-t-il un moyen de recevoir un rappel de react-router lorsque l'URL est mise à jour?

Aris
la source
Quelle version de react-router utilisez-vous? Cela déterminera la meilleure approche. Je fournirai une réponse une fois que vous aurez mis à jour. Cela étant dit, le withRouter HoC est probablement votre meilleur choix pour rendre un composant conscient de l'emplacement. Il mettra à jour votre composant avec de nouveaux ({match, history, and location}) chaque fois qu'un itinéraire change. De cette façon, vous n'avez pas besoin de vous inscrire et de vous désabonner manuellement aux événements. Cela signifie qu'il est facile à utiliser avec des composants sans état fonctionnels ainsi qu'avec des composants de classe.
Kyle Richardson le

Réponses:

114

Vous pouvez utiliser la history.listen()fonction lorsque vous essayez de détecter le changement d'itinéraire. Considérant que vous utilisez react-router v4, enveloppez votre composant avec withRouterHOC pour accéder à l' historyaccessoire.

history.listen()renvoie une unlistenfonction. Vous utiliseriez cela pour unregisterécouter.

Vous pouvez configurer vos itinéraires comme

index.js

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

puis dans 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);

À partir des documents d' histoire :

Vous pouvez écouter les modifications apportées à l'emplacement actuel en utilisant 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}`)
})

L'objet location implémente un sous-ensemble de l'interface window.location, comprenant:

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

Les emplacements peuvent également avoir les propriétés suivantes:

location.state - Un état supplémentaire pour cet emplacement qui ne réside pas dans l'URL (pris en charge dans createBrowserHistoryet createMemoryHistory)

location.key- Une chaîne unique représentant cet emplacement (prise en charge dans createBrowserHistoryet createMemoryHistory)

L'action PUSH, REPLACE, or POPdépend de la manière dont l'utilisateur est arrivé à l'URL actuelle.

Lorsque vous utilisez réagir-routeur v3 , vous pouvez utiliser history.listen()du historypaquet comme mentionné ci - dessus ou vous pouvez également utiliserbrowserHistory.listen()

Vous pouvez configurer et utiliser vos itinéraires comme

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>
        )
    }
} 
Shubham Khatri
la source
il utilise la v3 et la deuxième phrase de votre réponse dit " Considérant que vous utilisezreact-router v4 "
Kyle Richardson
1
@KyleRichardson Je pense que tu m'as encore mal compris, je dois certainement travailler mon anglais. Je voulais dire que si vous utilisez react-router v4 et que vous utilisez un objet d'historique, vous devez envelopper votre composant avecwithRouter
Shubham Khatri
@KyleRichardson Je vous vois ma réponse complète, j'ai ajouté des moyens de le faire dans la v3 également. Une dernière chose, l'OP a commenté qu'il utilise la v3 aujourd'hui et j'avais répondu à la question hier
Shubham Khatri
1
@ShubhamKhatri Oui, mais la façon dont votre réponse se lit est fausse. Il n'utilise pas la v4 ... Aussi, pourquoi utiliseriez-vous history.listen()lorsque vous utilisez withRouterdéjà des mises à jour de votre composant avec de nouveaux accessoires à chaque fois que le routage se produit? Vous pouvez faire une simple comparaison de nextProps.location.href === this.props.location.hrefin componentWillUpdatepour effectuer tout ce que vous devez faire s'il a changé.
Kyle Richardson
1
@Aris, avez-vous eu un changement pour l'essayer
Shubham Khatri
35

Mise à jour pour React Router 5.1.

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

const App = () => {
  const location = useLocation();

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

  return (
    <Switch>
      {/* Routes go here */}
    </Switch>
  );
};
onosendi
la source
13

Si vous souhaitez écouter l' historyobjet globalement, vous devrez le créer vous-même et le transmettre au Router. Ensuite, vous pouvez l'écouter avec sa listen()méthode:

// 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>

Encore mieux si vous créez l'objet historique en tant que module, vous pouvez donc facilement l'importer partout où vous en avez besoin (par exemple import history from './history';

Fabian Schultz
la source
9

react-router v6

Dans la prochaine v6 , cela peut être fait en combinant les crochets useLocationetuseEffect

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])
  ...
}

Pour une réutilisation pratique, vous pouvez le faire dans un useLocationChangecrochet personnalisé

// 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) 
  })
  ...
}

Si vous avez également besoin de voir l'itinéraire précédent lors du changement, vous pouvez le combiner avec un usePreviouscrochet

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) 
  })
  ...
}

Il est important de noter que tout ce qui précède se déclenche sur la première route client en cours de montage, ainsi que sur les modifications ultérieures. Si c'est un problème, utilisez le dernier exemple et vérifiez que a prevLocationexiste avant de faire quoi que ce soit.

Davnicwil
la source
J'ai une question. Si plusieurs composants ont été rendus et qu'ils regardent tous useLocation, tous leurs effets useEffects seront déclenchés. Comment puis-je vérifier que cet emplacement est correct pour le composant spécifique qui sera affiché?
Kex le
1
Hey @Kex - juste pour clarifier, locationvoici l'emplacement du navigateur, donc c'est le même dans chaque composant et toujours correct dans ce sens. Si vous utilisez le hook dans différents composants, ils recevront tous les mêmes valeurs lorsque l'emplacement change. Je suppose que ce qu'ils font avec ces informations sera différent, mais c'est toujours cohérent.
davnicwil le
Ça a du sens. Je me demande simplement comment un composant saura si le changement d'emplacement est pertinent pour lui-même effectuer une action. Par exemple, un composant reçoit un tableau de bord / une liste mais comment sait-il s'il est lié à cet emplacement ou non?
Kex le
Sauf si je fais quelque chose comme if (location.pathName === “dashboard / list”) {..... actions}. Cependant, cela ne semble pas très élégant chemin de codage en dur vers un composant.
Kex le
8

C'est une vieille question et je ne comprends pas tout à fait le besoin commercial d'écouter les changements d'itinéraire pour pousser un changement d'itinéraire; semble rond-point.

MAIS si vous vous êtes retrouvé ici parce que tout ce que vous vouliez était de mettre à jour le 'page_path'changement de route sur un routeur de réaction pour google analytics / global site tag / quelque chose de similaire, voici un hook que vous pouvez maintenant utiliser. Je l'ai écrit sur la base de la réponse acceptée:

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])
}

Vous devez utiliser ce hook une fois dans votre application, quelque part près du sommet mais toujours à l'intérieur d'un routeur. Je l'ai sur un App.jsqui ressemble à ceci:

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>
)
Johnny Magrippis
la source
1

Je suis tombé sur cette question alors que j'essayais de concentrer le lecteur d'écran ChromeVox en haut de «l'écran» après avoir navigué vers un nouvel écran dans une application React à une seule page. Essentiellement, essayer d'émuler ce qui se passerait si cette page était chargée en suivant un lien vers une nouvelle page Web rendue par le serveur.

Cette solution ne nécessite aucun écouteur, elle utilise withRouter()et la componentDidUpdate()méthode du cycle de vie pour déclencher un clic pour concentrer ChromeVox sur l'élément souhaité lors de la navigation vers un nouveau chemin d'URL.


la mise en oeuvre

J'ai créé un composant "Screen" qui est enroulé autour de la balise de commutation react-router qui contient tous les écrans des applications.

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

Screen.tsx Composant

Remarque: ce composant utilise 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)

J'avais essayé d'utiliser à la focus()place de click(), mais le clic fait que ChromeVox arrête de lire ce qu'il est en train de lire et recommence là où je lui dis de commencer.

Note avancée: Dans cette solution, la navigation <nav>qui à l'intérieur du composant Screen et rendue après le <main>contenu est visuellement positionnée au-dessus de l' mainutilisation de css order: -1;. Donc en pseudo code:

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

Si vous avez des idées, des commentaires ou des conseils sur cette solution, veuillez ajouter un commentaire.

Beau Smith
la source
1

React Router V5

Si vous voulez que le pathName soit une chaîne ('/' ou 'users'), vous pouvez utiliser ce qui suit:

  // React Hooks: React Router DOM
  let history = useHistory();
  const location = useLocation();
  const pathName = location.pathname;
jefelewis
la source
0
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])
}
yogesh panchal
la source