Wykryj zmianę trasy za pomocą routera React

87

Muszę zaimplementować logikę biznesową w zależności od historii przeglądania.

To, co chcę zrobić, to coś takiego:

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

Czy istnieje sposób na otrzymanie oddzwaniania z routera reaktywnego po zaktualizowaniu adresu URL?

Aris
źródło
Jakiej wersji React-routera używasz? To określi najlepsze podejście. Udzielę odpowiedzi, gdy zaktualizujesz. To powiedziawszy, withRouter HoC jest prawdopodobnie najlepszym sposobem na uświadomienie lokalizacji komponentu. Zaktualizuje komponent o nowe ({mecz, historia i lokalizacja}) za każdym razem, gdy zmieni się trasa. W ten sposób nie musisz ręcznie subskrybować i wypisywać się z wydarzeń. Oznacza to, że jest łatwy w użyciu z funkcjonalnymi komponentami bezstanowymi, a także komponentami klas.
Kyle Richardson

Odpowiedzi:

114

Możesz skorzystać z history.listen()funkcji, próbując wykryć zmianę trasy. Biorąc pod uwagę, że używasz react-router v4, zapakuj swój komponent za pomocą withRouterHOC, aby uzyskać dostęp do właściwości history.

history.listen()zwraca unlistenfunkcję. Używałbyś tego unregisterze słuchania.

Możesz skonfigurować swoje trasy, takie jak

index.js

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

a następnie w 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);

Z dokumentów historii :

Możesz nasłuchiwać zmian w bieżącej lokalizacji za pomocą 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}`)
})

Obiekt location implementuje podzbiór interfejsu window.location, w tym:

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

Lokalizacje mogą mieć również następujące właściwości:

location.state - Dodatkowy stan dla tej lokalizacji, który nie znajduje się w adresie URL (obsługiwane w createBrowserHistoryi createMemoryHistory)

location.key- Unikalny ciąg reprezentujący tę lokalizację (obsługiwany w createBrowserHistoryi createMemoryHistory)

Akcja zależy od PUSH, REPLACE, or POPtego, w jaki sposób użytkownik dotarł do bieżącego adresu URL.

Kiedy używasz React-router v3, możesz skorzystać history.listen()z historypakietu z pakietu, jak wspomniano powyżej, lub możesz również skorzystaćbrowserHistory.listen()

Możesz konfigurować i używać swoich tras, takich jak

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
źródło
używa wersji 3, a drugie zdanie Twojej odpowiedzi brzmi: „ Biorąc pod uwagę, że używaszreact-router v4
Kyle Richardson
1
@KyleRichardson Myślę, że znowu mnie źle zrozumiałeś, na pewno muszę popracować nad moim angielskim. Chodziło mi o to, że jeśli używasz reaktora routera v4 i używasz obiektu historii, to musisz opakować swój komponentwithRouter
Shubham Khatri
@KyleRichardson Widzę moją pełną odpowiedź, dodałem również sposoby, aby to zrobić w wersji 3. Jeszcze jedno, OP skomentował, że dzisiaj używa v3 i wczoraj odpowiedziałem na to pytanie
Shubham Khatri
1
@ShubhamKhatri Tak, ale sposób, w jaki twoja odpowiedź jest odczytywana, jest zły. On nie używa v4 ... Poza tym, dlaczego miałbyś używać, history.listen()gdy używasz withRouterjuż aktualizacji komponentu o nowe rekwizyty za każdym razem, gdy występuje routing? Możesz zrobić proste porównanie nextProps.location.href === this.props.location.hrefw, componentWillUpdateaby wykonać wszystko, co musisz zrobić, jeśli uległo zmianie.
Kyle Richardson
1
@Aris, czy dostałeś jakąś zmianę, żeby to wypróbować
Shubham Khatri
35

Aktualizacja dla 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
źródło
13

Jeśli chcesz słuchać historyobiektu globalnie, musisz sam go stworzyć i przekazać do Router. Następnie możesz posłuchać jej listen()metodą:

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

Jeszcze lepiej, jeśli utworzysz obiekt historii jako moduł, dzięki czemu możesz go łatwo zaimportować w dowolnym miejscu (np import history from './history';

Fabian Schultz
źródło
9

react-router v6

W nadchodzącym v6 , można to zrobić przez połączenie useLocationi useEffecthaki

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

W celu wygodnego ponownego wykorzystania możesz to zrobić w niestandardowym useLocationChangehaku

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

Jeśli chcesz również zobaczyć poprzednią trasę przy zmianie, możesz połączyć ją z usePrevioushakiem

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

Ważne jest, aby pamiętać, że wszystkie powyższe wywołują pożar na pierwszej montowanej trasie klienta, a także kolejne zmiany. Jeśli to problem, skorzystaj z drugiego przykładu i sprawdź, czy prevLocationistnieje, zanim cokolwiek zrobisz.

davnicwil
źródło
Mam pytanie. Jeśli wyrenderowano kilka komponentów i wszystkie one obserwują useLocation, zostaną uruchomione wszystkie ich właściwości useEffects. Jak sprawdzić, czy ta lokalizacja jest poprawna dla określonego składnika, który zostanie wyświetlony?
Kex
1
Hej @Kex - dla wyjaśnienia locationtutaj jest lokalizacja przeglądarki, więc jest taka sama w każdym komponencie i zawsze poprawna w tym sensie. Jeśli użyjesz haka w różnych komponentach, wszystkie otrzymają te same wartości po zmianie lokalizacji. Myślę, że to, co zrobią z tymi informacjami, będzie inne, ale zawsze jest spójne.
davnicwil
To ma sens. Zastanawiam się tylko, skąd komponent będzie wiedział, czy zmiana lokalizacji jest odpowiednia dla niego wykonującego działanie. Np. Komponent otrzymuje pulpit nawigacyjny / listę, ale skąd wie, czy jest powiązany z tą lokalizacją, czy nie?
Kex
Chyba że zrobię coś takiego jak if (location.pathName === „dashboard / list”) {..... działania}. Nie wydaje się jednak zbyt eleganckiej, twardej ścieżki do komponentu.
Kex
8

To stare pytanie i nie do końca rozumiem biznesową potrzebę nasłuchiwania zmian tras w celu wprowadzenia zmian; wydaje się okrężna.

ALE jeśli trafiłeś tutaj, ponieważ wszystko, czego chciałeś, to zaktualizować 'page_path'zmianę trasy na routerze reagującym na Google Analytics / globalny tag witryny / coś podobnego, oto haczyk, którego możesz teraz użyć. Napisałem to na podstawie zaakceptowanej odpowiedzi:

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

Powinieneś użyć tego haka raz w swojej aplikacji, gdzieś u góry, ale nadal wewnątrz routera. Mam to na, App.jsktóry wygląda tak:

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
źródło
1

Natknąłem się na to pytanie, gdy próbowałem ustawić czytnik ekranu ChromeVox na górze „ekranu” po przejściu do nowego ekranu w aplikacji jednostronicowej React. Zasadniczo próba emulacji tego, co by się stało, gdyby ta strona została załadowana, podążając za linkiem do nowej strony internetowej renderowanej przez serwer.

To rozwiązanie nie wymaga żadnych odbiorników, wykorzystuje withRouter()i componentDidUpdate()metodę cyklu życia, aby wywołać kliknięcie, aby skupić ChromeVox na żądanym elemencie podczas przechodzenia do nowej ścieżki adresu URL.


Realizacja

Utworzyłem komponent „Screen”, który jest owinięty wokół tagu przełącznika reagującego na router, który zawiera wszystkie ekrany aplikacji.

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

Screen.tsx Składnik

Uwaga: ten składnik używa 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)

Próbowałem użyć focus()zamiastclick() , ale kliknięcie powoduje, że ChromeVox przestaje czytać to, co aktualnie czyta, i zaczyna od nowa od miejsca, w którym każę mu zacząć.

Uwaga zaawansowana: w tym rozwiązaniu nawigacja <nav>wewnątrz komponentu Screen i renderowana po <main>treści jest wizualnie umieszczana nad mainusing css order: -1;. Czyli w pseudokodzie:

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

Jeśli masz jakieś przemyślenia, uwagi lub wskazówki dotyczące tego rozwiązania, dodaj komentarz.

Beau Smith
źródło
1

React Router V5

Jeśli chcesz, aby pathName był ciągiem („/” lub „users”), możesz użyć następującego:

  // React Hooks: React Router DOM
  let history = useHistory();
  const location = useLocation();
  const pathName = location.pathname;
jefelewis
źródło
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
źródło