使用react-router检测路由变化

194

我需要根据浏览历史记录实现一些业务逻辑。

我想要做的事情就像这样:

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

当URL更新时,有没有办法从react-router接收回调?


你使用的是哪个版本的react-router?这将决定最佳方法。一旦你更新,我会提供答案。话虽如此,withRouter 高阶组件可能是使组件具有位置感知能力的最佳选择。它会在路由更改时使用新的({match, history, and location })更新你的组件。这样,你就不需要手动订阅和取消订阅事件。这意味着它易于与功能状态组件以及类组件一起使用。 - Kyle Richardson
10个回答

176

当您尝试检测路由更改时,可以利用history.listen()函数。考虑到您正在使用react-router v4,请使用withRouter HOC包装您的组件,以获取访问history属性的权限。

history.listen()返回一个unlisten函数,您可以将其用于取消监听。

您可以配置您的路由:

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对象实现了窗口位置接口的一个子集,其中包括:

**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中支持)

动作是根据用户如何到达当前URL而确定的,可能是PUSH, REPLACE或POP之一。

当您使用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>
        )
    }
} 

1
他正在使用v3,你回答的第二句话说“_考虑到你正在使用react-router v4_”。 - Kyle Richardson
2
@KyleRichardson 我觉得你又误解了我的意思,我肯定需要改进我的英语。我的意思是,如果你正在使用 react-router v4,并且正在使用 history 对象,那么你需要用 withRouter 来包装你的组件。 - Shubham Khatri
1
@KyleRichardson 如果你看到我的完整答案,我已经添加了在v3中执行此操作的方法。还有一件事,OP评论说他今天正在使用v3,而我昨天回答了这个问题。 - Shubham Khatri
1
@ShubhamKhatri 是的,但你回答的方式是错误的。他没有使用v4……另外,当使用withRouter时,为什么要使用history.listen(),它已经在每次路由发生时使用新的props更新了您的组件?您可以在componentWillUpdate中进行nextProps.location.href === this.props.location.href的简单比较,以执行任何需要执行的操作,如果它已更改。 - Kyle Richardson
我使用了React Router v4和React Router Config。但是我无法使用withRouter来包装我的App组件。 它会引发以下错误:Uncaught Error: You should not use <Route> or withRouter() outside a <Router>。我的App组件渲染逻辑如下: return <BrowserRouter>{renderRoutes(routes)}</BrowserRouter>; - M.Abulsoud
显示剩余14条评论

175

React Router 5.1+的更新。

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function SomeComponent() {
  const location = useLocation();

  useEffect(() => {
    console.log('Location changed');
  }, [location]);

  ...
}

当您尝试检测未更改的路由时,此解决方案完美运作! - Amir Fahmideh
1
当用户询问时,如果您尝试在useEffect中执行history.push(),则会导致无限循环,因此我不认为这是一个解决方案。 - Cristian Muscalu
@Cristian Muscalu,尝试在依赖数组中将location替换为location.pathname - undefined

127

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

需要注意的是,所有上述内容都会在第一个客户端路由被挂载以及随后的更改时触发。如果这是个问题,请使用后面的示例,并在进行任何操作之前检查是否存在prevLocation


1
嘿@Kex - 只是为了澄清,这里的location是浏览器位置,因此在每个组件中都是相同的,并且在这个意义上始终正确。如果您在不同的组件中使用该钩子,当位置发生变化时,它们都将接收到相同的值。我猜他们对这些信息的处理方式会有所不同,但它始终是一致的。 - davnicwil
除非我做像 if (location.pathName === “dashboard/list”) { ..... actions } 这样的事情。但是硬编码组件路径似乎不太优雅。 - Kex
使用 useLocation/useEffect 方法有助于解决单页 Ionic 5 React 项目(使用 useParams)中“第一次加载后没有更改”的问题。在第一次加载后,Ionic 生命周期方法都不会被触发。 - naaman
3
如何使用TS实现相同的useLocationChange?同时,React出现了以下问题:React Hook useEffect缺少依赖项:'action'。要么包括它,要么删除依赖项数组。如果'action'经常发生变化,请查找定义它的父组件,并在其中包装该定义的useCallback react-hooks/exhaustive-deps。 - Ruben Marcus
我喜欢这个答案,最后的代码片段非常有用。但是,正如@RubenMarcus所说,有一个警告。如何在没有警告的情况下使其工作? - mudin
显示剩余2条评论

19

如果您想全局监听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';),这会更好。


4
什么情况下需要调用 unlisten() 方法?整个应用程序卸载时吗? - connected_user

13

这是一个老问题,我不太理解需要监听路由变化并推送路由变化的业务需求;似乎有些绕弯子。

但是,如果您来到这里是因为您想要在react-router路由变化时更新'page_path'以供谷歌分析/全局站点标签/类似功能使用,那么这里有一个钩子可以使用。我基于被接受的答案编写了它:

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()在路由更改时再次被调用? - Eric. M

2

使用useLocation() Hook来检测URL的变化,并将其放入useEffect()中的依赖数组中,这个技巧对我很有效。

const App = () => {

  const location = useLocation();

  useEffect(() => {
    window.scroll(0,0);
  }, [location]);

  return (
    <React.Fragment>
      <Routes>
        <Route path={"/"} element={<Template/>} >
          <Route index={true} element={<Home/>} />
          <Route path={"cart"} element={<Cart/>} />
          <Route path={"signin"} element={<Signin/>} />
          <Route path={"signup"} element={<Signup/>} />
          <Route path={"product/:slug"} element={<Product/>} />
          <Route path={"category/:category"} element={<ProductList/>} />
        </Route>
      </Routes>
    </React.Fragment>
  );
}

export default App;

1

您可以在类组件中使用 componentDidUpdateuseLocation 来获取路由变化,而在函数式组件中则可以使用 useEffect

在类组件中

import { useLocation } from "react-router";

class MainApp extends React.Component {
    constructor(props) {
       super(props);
    }
    async componentDidUpdate(prevProps) {
       if(this.props.location.pathname !== prevProps.location.pathname)
       {
         //route has been changed. do something here
       } 
    }
}


function App() {
 const location = useLocation()
 return <MainApp location={location} />
}

在函数式组件中

function App() {
     const location = useLocation()
     useEffect(() => {
        //route change detected. do something here
     }, [location]) //add location in dependency. It detects the location change
     return <Routes> 
              <Route path={"/"} element={<Home/>} >
              <Route path={"login"} element={<Login/>} />
            </Routes>
}

1
我在尝试将ChromeVox屏幕阅读器聚焦到React单页应用程序中的新屏幕后,遇到了这个问题。基本上是试图模拟通过链接加载新的服务器呈现的网页时会发生什么。
这个解决方案不需要任何监听器,它使用withRouter()componentDidUpdate()生命周期方法,在导航到新的URL路径时触发点击以使ChromeVox聚焦于所需元素。

实现

我创建了一个“屏幕”组件,它包装在 react-router switch 标签周围,其中包含所有应用程序的屏幕。

<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>位于屏幕组件内部并在<main>内容之后呈现,通过css order: -1;进行视觉定位。因此,在伪代码中:
<Screen style={{ display: 'flex' }}>
  <main>
  <nav style={{ order: -1 }}>
<Screen>

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

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

-1

React Router V5

如果你想要将路径名作为字符串('/'或'users')使用,可以使用以下代码:

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

1
这如何应用于检测路由变化? - vancy-pants
pathname = window.location.pathname - sao

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接