使用 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 中,这可以通过组合 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
是否存在。
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])
}
我必须根据浏览历史实现一些业务逻辑。
我想做的是这样的:
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
andcreateMemoryHistory
)
location.key
- A unique string representing this location (supported increateBrowserHistory
andcreateMemoryHistory
)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 中,这可以通过组合 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
是否存在。
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])
}