【问题标题】:Detect Route Change with react-router使用 react-router 检测路由变化
【发布时间】:2018-01-04 12:54:52
【问题描述】:

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

我想做的是这样的:

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

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

【问题讨论】:

  • 你使用的是什么版本的 react-router?这将确定最佳方法。更新后我会提供答案。话虽如此,withRouter HoC 可能是让组件位置感知的最佳选择。每当路线更改时,它将使用新的({match, history, and location })更新您的组件。这样您就不需要手动订阅和取消订阅事件。这意味着它很容易与功能性无状态组件以及类组件一起使用。

标签: reactjs react-router react-router-v4 react-router-redux react-router-dom


【解决方案1】:

您可以在尝试检测路线变化时使用history.listen() 函数。考虑到您使用的是react-router v4,请使用withRouter HOC 包装您的组件以访问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);

来自历史docs

您可以使用以下命令监听当前位置的变化 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 中(在 createBrowserHistorycreateMemoryHistory)

location.key - 表示此位置的唯一字符串(支持 在createBrowserHistorycreateMemoryHistory)

该操作是PUSH, REPLACE, or POP 之一,具体取决于用户的方式 到了当前的 URL。

当你使用 react-router v3 时,你可以使用上面提到的 history 包中的 history.listen() 或者你也可以使用 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
  • @KyleRichardson 我想你又误会了我,我当然要努力学习我的英语。我的意思是,如果您使用 react-router v4 并且您正在使用历史对象,那么您需要使用 withRouter 包装您的组件
  • @KyleRichardson 我你看到了我的完整答案,我也在 v3 中添加了方法。还有一件事,OP 评论说他今天使用的是 v3,而我昨天已经回答了这个问题
  • @ShubhamKhatri 是的,但是您的答案读取方式是错误的。他没有使用 v4 ......另外,当使用 withRouter 时,你为什么还要使用 history.listen() 已经在每次路由发生时用新的道具更新你的组件?您可以对componentWillUpdate 中的nextProps.location.href === this.props.location.href 进行简单比较,以执行您需要做的任何事情(如果它发生了变化)。
  • @Aris,你换了试试看
【解决方案2】:

如果您想在全局范围内收听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';

【讨论】:

  • 什么时候必须调用 unlisten()?整个应用何时卸载?
【解决方案3】:

我在导航到 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 停止阅读当前正在阅读的内容,并从我告诉它开始的地方重新开始。

高级说明:在此解决方案中,在屏幕组件内并在&lt;main&gt; 内容之后呈现的导航&lt;nav&gt; 使用css order: -1; 视觉定位在main 上方。所以在伪代码中:

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

如果您对此解决方案有任何想法、cmets 或提示,请添加评论。

【讨论】:

    【解决方案4】:

    这是一个老问题,我不太了解监听路由更改以推送路由更改的业务需求;似乎是迂回的。

    但是如果你最终来到这里是因为你想要的只是更新 'page_path' 上的 react-router 路由更改以用于谷歌分析/全局站点标签/类似的东西,这里有一个 hook 你可以现在使用。我是根据接受的答案写的:

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

    【讨论】:

    • 我已经尝试过您的代码,但它无法检测到我何时更改路线。当我刷新页面时它可以工作。但是什么时候更改路由,在 app.js 中不会再次调用 useTracking(),有没有办法让我在路由更改时再次调用 useTracking()?
    【解决方案5】:

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

    【讨论】:

      【解决方案6】:

      React 路由器 V5

      如果您希望 pathName 为字符串('/' 或 'users'),您可以使用以下内容:

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

      【讨论】:

      • 这如何应用于检测路线变化?
      • 路径名 = window.location.pathname
      【解决方案7】:

      react-router v6

      在 react-router 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) 
        })
        ...
      }
      

      请务必注意,上述所有触发均在 first 正在挂载的客户端路由上触发,以及后续更改。如果这是一个问题,请使用后一个示例并在执行任何操作之前检查 prevLocation 是否存在。

      【讨论】:

      • 我有一个问题。如果已经渲染了多个组件并且它们都在监视 useLocation,那么它们的所有 useEffects 都将被触发。如何验证此位置对于将要显示的特定组件是否正确?
      • 嘿@Kex - 只是为了澄清location 这里是浏览器位置,所以它在每个组件中都是相同的,并且在这个意义上总是正确的。如果你在不同的组件中使用钩子,当位置改变时,它们都会收到相同的值。我猜他们对这些信息的处理方式会有所不同,但始终是一致的。
      • 这是有道理的。只是想知道组件如何知道位置更改是否与自身执行操作相关。例如,一个组件接收仪表板/列表,但它如何知道它是否与该位置相关联?
      • 除非我执行 if (location.pathName === "dashboard/list") { ..... actions } 之类的操作。不过,它似乎不是非常优雅的组件硬编码路径。
      • 如何使用 TS 来执行相同的 useLocationChange?并且 react 抱怨 React Hook useEffect 缺少依赖项:'action'。要么包含它,要么删除依赖数组。如果“动作”变化太频繁,找到定义它的父组件并将该定义包装在 useCallback react-hooks/exhaustive-deps 中
      【解决方案8】:
      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])
      }
      

      【讨论】:

        猜你喜欢
        • 2018-12-29
        • 1970-01-01
        • 1970-01-01
        • 2020-06-11
        • 2016-04-12
        • 1970-01-01
        • 2017-12-27
        • 2019-11-27
        • 2022-07-07
        相关资源
        最近更新 更多