使用react-router检测路由更改


87

我必须根据浏览历史实现一些业务逻辑。

我想做的是这样的:

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

URL更新后,有什么方法可以接收来自react-router的回调吗?


您正在使用哪个版本的react-router?那将确定最佳方法。更新后,我会提供答案。话虽如此,withRouter HoC可能是使组件位置感知的最佳选择。每当路线更改时,它将使用新的{{match,history和location})更新您的组件。这样,您无需手动订阅和取消订阅事件。意味着可以很容易地与功能性无状态组件以及类组件一起使用。
凯尔·理查森

Answers:


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

location对象实现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的操作之一。

当您使用react-router v3时,您可以使用如上所述的history.listen()fromhistory包,也可以使用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
Kyle Richardson

1
@KyleRichardson我认为您再次误解了我,我当然必须努力学习我的英语。我的意思是,如果您使用的是react-router v4,并且您使用的是History对象,则需要用withRouter
Shubham Khatri

@KyleRichardson我看到了完整的答案,我也添加了在v3中执行此操作的方法。另一件事是,OP评论说他今天正在使用v3,而我昨天已经回答了这个问题
Shubham Khatri

1
@ShubhamKhatri是的,但是您的答案读错了。他没有使用v4...。此外,为什么每次布线history.listen()时使用withRouter已经使用新道具更新组件的零件时,为什么要使用?您可以对nextProps.location.href === this.props.location.hrefincomponentWillUpdate进行简单的比较,以执行更改后需要执行的所有操作。
凯尔·理查森

1
@Aris,您有找零的尝试吗
Shubham Khatri

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

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中,这可以通过结合useLocationuseEffect钩子来完成

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在执行任何操作之前检查a是否存在。


我有个问题。如果已经渲染了多个组件,并且它们都在监视useLocation,那么将触发其所有useEffects。如何验证此位置对于将要显示的特定组件是正确的?
Kex

1
嘿@Kex-只是在location这里澄清一下浏览器的位置,因此每个组件中的位置都相同,并且在这种意义上始终正确。如果在不同的组件中使用该挂钩,则它们在位置更改时都会收到相同的值。我想他们对该信息的处理方式会有所不同,但始终是一致的。
davnicwil

那讲得通。只是想知道组件如何知道位置更改是否与其自身执行操作有关。例如,一个组件收到仪表板/列表,但是如何知道它是否绑定到该位置?
Kex

除非我做类似if(location.pathName ===“仪表板/列表”){..... actions}的操作。但是,对于组件来说,这似乎不是非常优雅的硬编码路径。
Kex

8

这是一个古老的问题,我不太了解聆听路线变更以推动路线变更的业务需求。好像是回旋处。

但是,如果您由于想要更新'page_path'Google Google Analytics(分析)/全局站点标签/类似内容而在React-Router路由上进行更改而在这里结束,则现在可以使用此钩子。我根据公认的答案写了它:

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

我在React单页应用程序中导航到新屏幕后试图将ChromeVox屏幕阅读器聚焦到“屏幕”顶部时遇到了这个问题。基本上,尝试通过链接到新的服务器呈现的网页的链接来模拟如果加载此页面时将发生的情况。

该解决方案不需要任何监听器,它使用withRouter()componentDidUpdate()生命周期的方法来触发点击导航到一个新的URL路径时关注的ChromeVox所需的元素。


实作

我创建了一个“屏幕”组件,该组件包裹在包含所有应用程序屏幕的react-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

反应路由器V5

如果要将pathName作为字符串(“ /”或“用户”),则可以使用以下命令:

  // React Hooks: React Router DOM
  let history = useHistory();
  const location = useLocation();
  const pathName = location.pathname;

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])
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.