Comment écouter les changements de route dans React Router v4?

138

J'ai quelques boutons qui agissent comme des itinéraires. Chaque fois que l'itinéraire est modifié, je veux m'assurer que le bouton actif change.

Existe-t-il un moyen d'écouter les changements d'itinéraire dans React Router v4?

Kasper
la source

Réponses:

170

J'utilise withRouterpour obtenir l' locationaccessoire. Lorsque le composant est mis à jour en raison d'un nouvel itinéraire, je vérifie si la valeur a changé:

@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>
  }
}

J'espère que ça aide

brickpop
la source
21
Utilisez 'this.props.location.pathname' dans react router v4.
ptorsson
4
@ledfusion Je fais la même chose et withRouterj'utilise, mais j'obtiens une erreur You should not use <Route> or withRouter() outside a <Router>. Je ne vois aucun <Router/>composant dans le code ci-dessus. Alors, comment ça marche?
maverick
1
Salut @maverick. Je ne sais pas à quoi ressemble votre code, mais dans l'exemple ci-dessus, le <Switch>composant agit en tant que routeur de facto. Seule la première <Route>entrée à avoir un chemin correspondant sera rendue. Aucun <Router/>composant n'est nécessaire dans ce scénario
brickpop
1
pour utiliser @withRouter, vous devez installer npm install --save-dev transform-decorators-legacy
Sigex
68

Pour développer ce qui précède, vous devrez accéder à l'objet historique. Si vous utilisez BrowserRouter, vous pouvez importer withRouteret envelopper votre composant avec un composant d'ordre supérieur (HoC) afin d'avoir accès via des accessoires aux propriétés et fonctions de l'objet historique.

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

La seule chose à savoir est que withRouter et la plupart des autres moyens d'accéder au historysemblent polluer les accessoires lorsqu'ils déstructurent l'objet en lui.

Sam Parmenter
la source
La réponse m'a aidé à comprendre quelque chose quelle que soit la question :). Mais fixez- withRoutesvous withRouter.
Sergey Reutskiy
Oui, désolé, merci de l'avoir signalé. J'ai modifié le message. J'ai mis l'importation correcte en haut de la question, puis je l'ai mal orthographiée dans l'exemple de code.
Sam Parmenter
5
Je pense que la version actuelle de withRouter passe historyplutôt que la variable listen.
mikebridge
5
Il serait bon de modifier le message pour démontrer le non-écoute; ce code contient une fuite de mémoire.
AndrewSouthpaw
34

Vous devez utiliser history v4 lib.

Exemple à partir de

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
la source
1
Les appels history.pushState () et history.replaceState () ne déclenchent pas l'événement popstate, donc cela seul ne couvrira pas tous les changements d'itinéraire.
Ryan
1
@Ryan Il semble que history.pushcela déclenche history.listen. Consultez l'exemple Utilisation d'une URL de base dans la documentation de l' historique v4 . Comme il historys'agit en fait d'un wrapper de l' historyobjet natif d'un navigateur, il ne se comporte pas exactement comme l'objet natif.
Rockallite
Cela semble être une meilleure solution, car vous avez souvent besoin d'écouter les changements d'itinéraire pour l'envoi d'événements, ce qui n'est pas lié à la réaction des événements du cycle de vie des composants.
Daniel Dubovski
12
Fuite de mémoire potentielle! Très important que vous fassiez cela! "Lorsque vous attachez un écouteur à l'aide de history.listen, il renvoie une fonction qui peut être utilisée pour supprimer l'écouteur, qui peut ensuite être appelée dans la logique de nettoyage:const unlisten = history.listen(myListener); unlisten();
Dehan de Croos
Allez ici pour la documentation sur le package historique. github.com/ReactTraining/history/blob/master/docs/…
Jason Kim
26

withRouter, history.listenEt useEffect(React crochets) fonctionne très bien ensemble:

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)

Le rappel de l'écouteur se déclenchera chaque fois qu'une route est modifiée, et le retour pour history.listenest un gestionnaire d'arrêt qui joue bien avec useEffect.

noetix
la source
13

v5.1 introduit le hook utile 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
la source
4
Juste une remarque que j'avais des problèmes avec une erreur: Cannot read property 'location' of undefined at useLocation. Vous devez vous assurer que l'appel useLocation () n'est pas dans le même composant qui met le routeur dans l'arborescence: voir ici
toddg
11

Avec crochets:

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)

Importer et rendre en tant que <DebugHistory>composant

Tymek
la source
11
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]);

}

avec crochets

Sodium United
la source
Holly Jesys! Comment ça marche? Votre réponse est cool! buti a mis le point de débogage dans useEffect mais chaque fois que j'ai changé le chemin, l'emplacement est resté indéfini ? pouvez-vous partager un bon article? car il est difficile de trouver des informations claires
AlexNikonov
7
import { useHistory } from 'react-router-dom';

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

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

  return null;
}
weiya ou
la source
Regarde-t-il également les changements de hachage? route / a # 1 -> route / a # 2
Naren
1

Avec React Hooks, j'utilise useEffect

  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])
Alan
la source
0

Dans certains cas, vous pouvez utiliser l' renderattribut au lieu de component, de cette manière:

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

Notez que si vous changez l'état de la onRouteChangeméthode, cela peut provoquer l'erreur «Profondeur de mise à jour maximale dépassée».

Fernando
la source
0

Avec le useEffecthook, il est possible de détecter les changements d'itinéraire sans ajouter d'écouteur.

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
la source
0

Je viens de traiter ce problème, je vais donc ajouter ma solution en complément des autres réponses données.

Le problème ici est que useEffectcela ne fonctionne pas vraiment comme vous le souhaiteriez, car l'appel n'est déclenché qu'après le premier rendu, il y a donc un délai indésirable.
Si vous utilisez un gestionnaire d'état comme redux, il y a de fortes chances que vous obteniez un scintillement à l'écran en raison d'un état persistant dans le magasin.

Ce que vous voulez vraiment, c'est utiliser useLayoutEffectcar cela se déclenche immédiatement.

J'ai donc écrit une petite fonction utilitaire que j'ai placée dans le même répertoire que mon routeur:

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

Ce que j'appelle depuis le composant HOC comme ceci:

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

pathest l'accessoire qui est passé dans l' matchobjet lors de l'utilisation withRouter.

Je ne suis pas vraiment en faveur de l'écoute / désécoute manuelle de l'histoire. C'est juste imo.

Trace
la source