Detectar mudança de rota com react-roteador

87

Tenho que implementar alguma lógica de negócios dependendo do histórico de navegação.

O que eu quero fazer é algo assim:

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

Existe alguma maneira de receber um retorno de chamada do react-router quando o URL é atualizado?

Aris
fonte
Qual versão do react-router você está usando? Isso determinará a melhor abordagem. Fornecerei uma resposta assim que você atualizar. Dito isso, o withRouter HoC é provavelmente sua melhor aposta para tornar ciente a localização de um componente. Ele atualizará seu componente com novos ({match, history, and location}) sempre que uma rota for alterada. Dessa forma, você não precisa se inscrever e cancelar manualmente os eventos. O que significa que é fácil de usar com componentes funcionais sem estado, bem como componentes de classe.
Kyle Richardson

Respostas:

114

Você pode usar a history.listen()função ao tentar detectar a mudança de rota. Considerando que você está usando react-router v4, envolva seu componente com withRouterHOC para obter acesso ao historyprop.

history.listen()retorna uma unlistenfunção. Você usaria isso para unregisterouvir.

Você pode configurar suas rotas como

index.js

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

e então em 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);

Dos documentos de história :

Você pode ouvir as alterações no local atual usando 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}`)
})

O objeto location implementa um subconjunto da interface window.location, incluindo:

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

Os locais também podem ter as seguintes propriedades:

location.state - algum estado extra para este local que não reside no URL (compatível com createBrowserHistorye createMemoryHistory)

location.key- Uma string única que representa este local (compatível com createBrowserHistoryecreateMemoryHistory )

A ação PUSH, REPLACE, or POPdepende de como o usuário chegou ao URL atual.

Quando você está usando o react-router v3, você pode fazer uso history.listen()do historypacote como mencionado acima ou você também pode fazer usobrowserHistory.listen()

Você pode configurar e usar suas rotas como

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
fonte
ele está usando a v3 e a segunda frase de sua resposta diz " Considerando que você está usandoreact-router v4 "
Kyle Richardson
1
@KyleRichardson Acho que você me entendeu mal de novo, com certeza tenho que melhorar meu inglês. Eu quis dizer que se você está usando o react-router v4 e está usando o objeto de histórico, então você precisa embrulhar seu componente comwithRouter
Shubham Khatri
@KyleRichardson Viu minha resposta completa, adicionei maneiras de fazer isso na v3 também. Mais uma coisa, o OP comentou que está usando a v3 hoje e eu tinha respondido a pergunta ontem
Shubham Khatri
1
@ShubhamKhatri Sim, mas a forma como sua resposta está correta. Ele não está usando a v4 ... Além disso, por que você usaria history.listen()quando withRouterjá usa atualiza seu componente com novos props toda vez que ocorre o roteamento? Você poderia fazer uma comparação simples de nextProps.location.href === this.props.location.hrefem componentWillUpdatepara realizar qualquer coisa que você precise fazer se tiver mudado.
Kyle Richardson
1
@Aris, você teve alguma mudança para experimentar
Shubham Khatri
35

Atualização para 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
fonte
13

Se você quiser ouvir o historyobjeto globalmente, terá que criá-lo e passá-lo para oRouter . Então você pode ouvi-lo com seu listen()método:

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

Melhor ainda se você criar o objeto de histórico como um módulo, para que possa importá-lo facilmente para qualquer lugar que precisar (por exemplo import history from './history';

Fabian Schultz
fonte
9

react-router v6

Na próxima v6 , isso pode ser feito combinando os ganchos useLocationeuseEffect

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

Para uma reutilização conveniente, você pode fazer isso em um useLocationChangegancho personalizado

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

Se você também precisa ver a rota anterior na mudança, você pode combiná-la com um usePreviousgancho

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

É importante observar que todos os itens acima são acionados na primeira rota do cliente que está sendo montada, bem como nas alterações subsequentes. Se isso for um problema, use o último exemplo e verifique se prevLocationexiste um antes de fazer qualquer coisa.

Davnicwil
fonte
Eu tenho uma pergunta. Se vários componentes tiverem sido renderizados e todos estiverem observando useLocation, todos os seus useEffects serão acionados. Como posso verificar se este local está correto para o componente específico que será exibido?
Kex
1
Olá @Kex - só para esclarecer locationaqui está a localização do navegador, então é a mesma em todos os componentes e sempre correta nesse sentido. Se você usar o gancho em componentes diferentes, todos eles receberão os mesmos valores quando a localização for alterada. Eu acho que o que eles fazem com essa informação será diferente, mas é sempre consistente.
davnicwil
Isso faz sentido. Apenas me perguntando como um componente saberá se a mudança de local é relevante para si mesmo ao executar uma ação. Por exemplo, um componente recebe painel / lista, mas como ele sabe se está vinculado a esse local ou não?
Kex
A menos que eu faça algo como if (location.pathName === “painel / lista”) {..... ações}. No entanto, não parece um caminho de codificação muito elegante para um componente.
Kex
8

Essa é uma questão antiga e não entendo muito bem a necessidade comercial de ouvir mudanças de rota para forçar uma mudança de rota; parece indireto.

MAS se você acabou aqui porque tudo o que queria era atualizar a 'page_path'mudança de rota em um roteador react para google analytics / tag global do site / algo semelhante, aqui está um gancho que você pode usar agora. Eu o escrevi com base na resposta aceita:

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

Você deve usar este gancho uma vez em seu aplicativo, em algum lugar próximo ao topo, mas ainda dentro de um roteador. Eu tenho em um App.jsque se parece com este:

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
fonte
1

Eu me deparei com essa pergunta enquanto tentava enfocar o leitor de tela ChromeVox na parte superior da "tela" após navegar para uma nova tela em um aplicativo React de página única. Basicamente tentando emular o que aconteceria se esta página fosse carregada, seguindo um link para uma nova página da web renderizada pelo servidor.

Esta solução não requer nenhum ouvinte, ela usa withRouter()e o componentDidUpdate()método de ciclo de vida para acionar um clique para focar o ChromeVox no elemento desejado ao navegar para um novo caminho de url.


Implementação

Eu criei um componente "Screen" que envolve a tag do switch react-router que contém todas as telas de aplicativos.

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

Screen.tsx Componente

Observação: este componente usa 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)

Tentei usar em focus()vez de click(), mas clicar faz com que o ChromeVox pare de ler tudo o que está lendo no momento e comece novamente onde eu disse para começar.

Nota avançada: nesta solução, a navegação <nav>que dentro do componente Tela e renderizada após o <main>conteúdo é visualmente posicionada acima do maincss usando order: -1;. Portanto, em pseudo código:

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

Se você tiver quaisquer ideias, comentários ou dicas sobre esta solução, adicione um comentário.

Beau Smith
fonte
1

Roteador React V5

Se desejar o pathName como uma string ('/' ou 'usuários'), você pode usar o seguinte:

  // React Hooks: React Router DOM
  let history = useHistory();
  const location = useLocation();
  const pathName = location.pathname;
Jefelewis
fonte
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
fonte