Обнаружение изменения маршрута с помощью реактивного маршрутизатора

87

Мне нужно реализовать некоторую бизнес-логику в зависимости от истории просмотров.

Я хочу сделать что-то вроде этого:

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

Есть ли способ получить обратный вызов от реактивного маршрутизатора при обновлении URL-адреса?

Арис
источник
Какую версию response-router вы используете? Это определит лучший подход. Я дам ответ, как только вы обновитесь. При этом withRouter HoC, вероятно, ваш лучший выбор для определения местоположения компонента. Он будет обновлять ваш компонент новыми ({совпадение, история и местоположение}) каждый раз, когда изменяется маршрут. Таким образом, вам не нужно вручную подписываться и отказываться от подписки на события. Это означает, что его легко использовать как с функциональными компонентами без состояния, так и с компонентами классов.
Кайл Ричардсон

Ответы:

114

Вы можете использовать history.listen()функцию при попытке обнаружить изменение маршрута. Учитывая, что вы используете react-router v4, оберните свой компонент withRouterHOC, чтобы получить доступ к historyопоре.

history.listen()возвращает unlistenфункцию. Вы бы использовали это для unregisterпрослушивания.

Вы можете настроить свои маршруты, например

index.js

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

а затем в 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);

Из исторической документации :

Вы можете прослушивать изменения в текущем местоположении, используя 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}`)
})

Объект местоположения реализует подмножество интерфейса window.location, включая:

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

Локации также могут иметь следующие свойства:

location.state - Некоторое дополнительное состояние для этого местоположения, которое не находится в URL (поддерживается в createBrowserHistoryи createMemoryHistory)

location.key- Уникальная строка, представляющая это местоположение (поддерживается в createBrowserHistoryи createMemoryHistory)

Действие PUSH, REPLACE, or POPзависит от того, как пользователь перешел на текущий URL.

Когда вы используете response-router v3, вы можете использовать history.listen()из historyпакета, как указано выше, или вы также можете использоватьbrowserHistory.listen()

Вы можете настраивать и использовать свои маршруты, например

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>
        )
    }
} 
Шубхам Хатри
источник
он использует v3, и второе предложение вашего ответа гласит: « Учитывая, что вы используетеreact-router v4 »
Кайл Ричардсон
1
@KyleRichardson Я думаю, вы снова меня неправильно поняли, мне определенно нужно поработать над своим английским. Я имел в виду, что если вы используете response-router v4 и используете объект истории, вам нужно обернуть свой компонент с помощьюwithRouter
Шубхам Хатри
@KyleRichardson Я вижу мой полный ответ, я также добавил способы сделать это в версии 3. Еще одна вещь, ОП прокомментировал, что он использует v3 сегодня, и я ответил на вопрос вчера
Шубхам Хатри
1
@ShubhamKhatri Да, но ваш ответ неверен. Он не использует v4 ... Кроме того, зачем вам использовать history.listen()при использовании withRouterуже обновлений вашего компонента с новыми реквизитами каждый раз, когда происходит маршрутизация? Вы можете провести простое сравнение nextProps.location.href === this.props.location.hrefin, componentWillUpdateчтобы выполнить все, что вам нужно, если оно изменилось.
Кайл Ричардсон
1
@Aris, у тебя есть сдача, чтобы попробовать?
Шубхам Хатри
35

Обновление для 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
источник
13

Если вы хотите прослушать historyобъект глобально, вам придется создать его самостоятельно и передать в Router. Затем вы можете прослушать его с помощью его listen()метода:

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

Еще лучше, если вы создадите объект истории как модуль, чтобы вы могли легко импортировать его в любое место, где он вам может понадобиться (например, import history from './history';

Фабиан Шульц
источник
9

react-router v6

В предстоящем v6 , это может быть сделано путем объединения useLocationи useEffectкрючков

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

Для удобства повторного использования вы можете сделать это в кастомном useLocationChangeхуке

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

Если вам тоже нужно видеть предыдущий маршрут при смене, можно комбинировать с usePreviousкрючком

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

Важно отметить, что все вышеперечисленное срабатывает на первом монтируемом клиентском маршруте, а также при последующих изменениях. Если это проблема, используйте последний пример и убедитесь, что prevLocationсуществует, прежде чем что-либо делать.

Давниквиль
источник
У меня есть вопрос. Если были отрисованы несколько компонентов, и все они наблюдают за useLocation, то сработают все их useEffects. Как я могу проверить правильность этого расположения для конкретного отображаемого компонента?
Kex
1
Привет, @Kex - просто для пояснения, locationвот расположение браузера, так что оно одинаково для всех компонентов и всегда правильно в этом смысле. Если вы используете ловушку в разных компонентах, все они получат одинаковые значения при изменении местоположения. Я предполагаю, что то, что они делают с этой информацией, будет другим, но это всегда согласованно.
davnicwil
Это имеет смысл. Просто интересно, как компонент узнает, относится ли изменение местоположения к самому выполнению действия. Например, компонент получает панель / список, но как он узнает, привязан он к этому месту или нет?
Kex
Если только я не сделаю что-то вроде if (location.pathName === «панель / список») {..... действия}. Однако это не кажется очень элегантным путем жесткого кодирования компонента.
Kex,
8

Это старый вопрос, и я не совсем понимаю, зачем бизнесу нужно прислушиваться к изменениям маршрута, чтобы подтолкнуть его; кажется окольным.

НО, если вы оказались здесь, потому что все, что вам нужно, это обновить 'page_path'изменение маршрута реагирующего маршрутизатора для аналитики Google / глобального тега сайта / чего-то подобного, вот крючок, который вы теперь можете использовать. Я написал это на основе принятого ответа:

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

Вы должны использовать этот хук один раз в своем приложении, где-то в верхней части, но все еще внутри маршрутизатора. У меня он App.jsвыглядит так:

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>
)
Джонни Магриппис
источник
1

Я столкнулся с этим вопросом, когда пытался сфокусировать программу чтения с экрана ChromeVox в верхней части «экрана» после перехода на новый экран в одностраничном приложении React. В основном это попытка имитировать то, что произошло бы, если бы эта страница была загружена путем перехода по ссылке на новую веб-страницу, отображаемую на сервере.

Это решение не требует никаких прослушивателей, оно использует withRouter()и componentDidUpdate()метод жизненного цикла для запуска щелчка, чтобы сфокусировать ChromeVox на желаемом элементе при переходе к новому URL-адресу.


Реализация

Я создал компонент «Экран», который обернут вокруг тега переключателя response-router, который содержит все экраны приложений.

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

Screen.tsx Составная часть

Примечание. Этот компонент использует 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)

Я пробовал использовать focus()вместо click(), но щелчок заставляет ChromeVox перестать читать все, что он сейчас читает, и начать снова с того места, где я сказал ему начать.

Дополнительное примечание: в этом решении навигация, <nav>которая находится внутри компонента Screen и отображается после <main>содержимого, визуально располагается над mainиспользованием css order: -1;. Итак, в псевдокоде:

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

Если у вас есть мысли, комментарии или советы по поводу этого решения, добавьте комментарий.

Бо Смит
источник
1

React Router V5

Если вы хотите, чтобы имя пути было строкой ('/' или 'пользователи'), вы можете использовать следующее:

  // React Hooks: React Router DOM
  let history = useHistory();
  const location = useLocation();
  const pathName = location.pathname;
Jefelewis
источник
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])
}
йогеш панчал
источник