使用 react-router 检测路由变化

Detect Route Change with react-router

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

我想做的是这样的:

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

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

您可以在尝试检测路线变化时使用history.listen() 功能。考虑到您正在使用 react-router v4,请使用 withRouter HOC 包装您的组件以访问 history 属性。

history.listen() returns 一个 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:

You can listen for changes to the current location using 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}`)
})

The location object implements a subset of the window.location interface, including:

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

Locations may also have the following properties:

location.state - Some extra state for this location that does not reside in the URL (supported in createBrowserHistory and createMemoryHistory)

location.key - A unique string representing this location (supported in createBrowserHistory and createMemoryHistory)

The action is one of PUSH, REPLACE, or POP depending on how the user got to the current 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>
        )
    }
} 

如果您想全局监听 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';

我在 React 单页应用程序导航到新屏幕后试图将 ChromeVox 屏幕 reader 聚焦到 "screen" 的顶部时遇到了这个问题。基本上试图模拟如果通过跟随 link 到一个新的服务器呈现的网页来加载此页面会发生什么。

此解决方案不需要任何侦听器,它使用 withRouter()componentDidUpdate() 生命周期方法来触发点击,以便在导航到新 url 路径。


实施

我创建了一个 "Screen" 组件,它包裹在包含所有应用程序屏幕的 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 停止阅读当前正在阅读的内容,并从我告诉它开始的地方重新开始。

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

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

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

这是一个老问题,我不太明白监听路由变化以推送路由变化的业务需求;好像是迂回的。

但是,如果您因为 google 分析 / 全局站点标签 / 类似的东西而在 react-router 路由更改上更新 'page_path' 而来到这里,这里有一个 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>
)

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

  ...
}

React Router V5

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

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

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 是否存在。

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