React Router v4 嵌套匹配参数无法在根级别访问
Posted
技术标签:
【中文标题】React Router v4 嵌套匹配参数无法在根级别访问【英文标题】:React Router v4 Nested match params not accessible at root level 【发布时间】:2019-03-12 01:08:15 【问题描述】:测试用例
https://codesandbox.io/s/rr00y9w2wm
重现步骤
点击Topics 点击Rendering with React或
转到https://rr00y9w2wm.codesandbox.io/topics/rendering预期行为
match.params.topicId
应与父 Topics 组件相同,在 Topic 组件中访问时应与 match.params.topicId
相同
实际行为
match.params.topicId
在 Topic 组件中访问时为 undefined
match.params.topicId
在 Topics 组件中访问时是 rendering
我从this closed issue 了解到,这不一定是错误。
此要求在想要在工厂 Web 应用程序中创建运行的用户中非常普遍,其中父级别的组件 Topics
需要访问 match.params.paramId 其中@987654334 @ 是匹配嵌套(子)组件 Topic
的 URL 参数:
const Topic = ( match ) => (
<div>
<h2>Topic ID param from Topic Components</h2>
<h3>match.params.topicId</h3>
</div>
);
const Topics = ( match ) => (
<div>
<h2>Topics</h2>
<h3>match.params.topicId || "undefined"</h3>
<Route path=`$match.url/:topicId` component=Topic />
...
</div>
);
在一般意义上,Topics
可以是抽屉或导航菜单组件,Topic
可以是任何子组件,就像在我正在开发的应用程序中一样。子组件有它自己的:topicId
参数,它有它自己的(比如说)<Route path="sections/:sectionId" component=Section />
Route/Component。
更痛苦的是,导航菜单不必与组件树建立一对一的关系。有时菜单根级别的项目(例如Topics
、Sections
等)可能对应于 嵌套 结构(Sections
仅在主题下呈现,/topics/:topicId/sections/:sectionId
虽然它有自己的规范化列表,用户可以在导航栏中的标题 Sections 下使用)。
因此,当 Sections 被点击时,it 应该被突出显示,而不是 Sections 和 Topics。
由于位于应用程序根级别的导航栏组件无法使用sectionId
或sections
路径,因此有必要为这种常见的用例编写hacks like this。
我根本不是 React Router 方面的专家,所以如果有人可以为这个用例冒险一个适当的优雅解决方案,我会认为这是一项富有成果的努力。优雅,我的意思是
使用match
而不是history.location.pathname
不涉及像手动解析window.location.xxx
这样的hacky方法
不使用this.props.location.pathname
不使用第三方库,如path-to-regexp
不使用查询参数
其他技巧/部分解决方案/相关问题:
React Router v4 - How to get current route?
React Router v4 global no match to nested route childs
TIA!
【问题讨论】:
【参考方案1】:React-router
不会为您提供任何匹配的子 Route 的匹配参数,而是根据当前匹配为您提供参数。因此,如果您有类似的路线设置
<Route path='/topic' component=Topics />
在Topics
组件中你有一个类似的路由
<Route path=`$match.url/:topicId` component=Topic />
现在,如果您的 url 是 /topic/topic1
,它与内部 Route 匹配,但对于 Topics 组件,匹配的 Route 仍然是 /topic
,因此其中没有参数,这是有道理的。
如果要获取主题组件中匹配的子路由的参数,则需要使用 React-router 提供的matchPath
实用程序并针对要获取其参数的子路由进行测试
import matchPath from 'react-router'
render()
const users, flags, location = this.props;
const match = matchPath(location.pathname,
path: '/topic/:topicId',
exact: true,
strict: false
)
if(match)
console.log(match.params.topicId);
return (
<div>
<Route exact path="/topic/:topicId" component=Topic />
</div>
)
编辑:
获取任何级别的所有参数的一种方法是利用上下文并在参数与上下文提供程序中匹配时更新它们。
您需要围绕 Route 创建一个包装器才能使其正常工作,典型示例如下所示
RouteWrapper.jsx
import React from "react";
import _ from "lodash";
import matchPath from "react-router-dom";
import ParamContext from "./ParamsContext";
import withRouter, Route from "react-router-dom";
class CustomRoute extends React.Component
getMatchParams = props =>
const location, path, exact, strict = props || this.props;
const match = matchPath(location.pathname,
path,
exact,
strict
);
if (match)
console.log(match.params);
return match.params;
return ;
;
componentDidMount()
const updateParams = this.props;
updateParams(this.getMatchParams());
componentDidUpdate(prevProps)
const updateParams, match = this.props;
const currentParams = this.getMatchParams();
const prevParams = this.getMatchParams(prevProps);
if (!_.isEqual(currentParams, prevParams))
updateParams(match.params);
componentWillUnmount()
const updateParams = this.props;
const matchParams = this.getMatchParams();
Object.keys(matchParams).forEach(k => (matchParams[k] = undefined));
updateParams(matchParams);
render()
return <Route ...this.props />;
const RouteWithRouter = withRouter(CustomRoute);
export default props => (
<ParamContext.Consumer>
( updateParams ) =>
return <RouteWithRouter updateParams=updateParams ...props />;
</ParamContext.Consumer>
);
ParamsProvider.jsx
import React from "react";
import ParamContext from "./ParamsContext";
export default class ParamsProvider extends React.Component
state =
allParams:
;
updateParams = params =>
console.log( params: JSON.stringify(params) );
this.setState(prevProps => (
allParams:
...prevProps.allParams,
...params
));
;
render()
return (
<ParamContext.Provider
value=
allParams: this.state.allParams,
updateParams: this.updateParams
>
this.props.children
</ParamContext.Provider>
);
索引.js
ReactDOM.render(
<BrowserRouter>
<ParamsProvider>
<App />
</ParamsProvider>
</BrowserRouter>,
document.getElementById("root")
);
Working DEMO
【讨论】:
感谢您的回答,舒巴姆。但这似乎不太友好,考虑到我需要创建每条路径两次,一次在路线上,然后在导航菜单上。我正在寻找 DRY 解决方案 @nikjohn 一个 DRY 友好的解决方案是使用上下文,我会在一段时间内更新我的答案以添加一个基于上下文的解决方案 @nikjohn,添加了使用上下文的 DRY 友好方法的演示,请看一下。它可能有一些错误,但我认为这对您在此基础上进行开发会有所帮助 我最终做了类似的事情,所以我奖励你赏金。作为记录,我仍然认为 React Router 4 方法不适合一些常见的用例,这让我很沮丧。我希望有人想出一种替代方法,例如 React Little Router(formidable.com/blog/2016/07/25/…) @nikjohn,我同意 react-router-v4 并非最适合所有用例,我们必须进行大量调整才能获得结果。上述解决方案就是其中之一【参考方案2】:尝试利用查询参数?
允许父母和孩子访问当前选定的topic
。不幸的是,您将需要使用模块 qs,因为 react-router-dom
不会自动解析查询(react-router v3 会)。
工作示例:https://codesandbox.io/s/my1ljx40r9
URL 的结构类似于串联字符串:
topic?topic=props-v-state
然后您将使用&
添加到查询中:
/topics/topic?topic=optimization&category=pure-components&subcategory=shouldComponentUpdate
✔ 使用匹配进行路由 URL 处理
✔ 不使用this.props.location.pathname
(使用this.props.location.search
)
✔ 使用qs
解析location.search
✔ 不涉及骇人听闻的方法
Topics.js
import React from "react";
import Link, Route from "react-router-dom";
import qs from "qs";
import Topic from "./Topic";
export default ( match, location ) =>
const topic = qs.parse(location.search,
ignoreQueryPrefix: true
);
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to=`$match.url/topic?topic=rendering`>
Rendering with React
</Link>
</li>
<li>
<Link to=`$match.url/topic?topic=components`>Components</Link>
</li>
<li>
<Link to=`$match.url/topic?topic=props-v-state`>
Props v. State
</Link>
</li>
</ul>
<h2>
Topic ID param from Topic<strong>s</strong> Components
</h2>
<h3>topic && topic</h3>
<Route
path=`$match.url/:topicId`
render=props => <Topic ...props topic=topic />
/>
<Route
exact
path=match.url
render=() => <h3>Please select a topic.</h3>
/>
</div>
);
;
另一种方法是创建一个HOC
,将参数存储到state
,当其参数发生变化时,子级会更新父级的state
。
URL 的结构类似于文件夹树:/topics/rendering/optimization/pure-components/shouldComponentUpdate
工作示例:https://codesandbox.io/s/9joknpm9jy
✔ 使用匹配进行路由 URL 处理
✔ 不使用this.props.location.pathname
✔ 使用 lodash 进行对象间比较
✔ 不涉及骇人听闻的方法
Topics.js
import map from "lodash/map";
import React, Fragment, Component from "react";
import NestedRoutes from "./NestedRoutes";
import Links from "./Links";
import createPath from "./createPath";
export default class Topics extends Component
state =
params: "",
paths: []
;
componentDidMount = () =>
const urlPaths = [
this.props.match.url,
":topicId",
":subcategory",
":item",
":lifecycles"
];
this.setState( paths: createPath(urlPaths) );
;
handleUrlChange = params => this.setState( params );
showParams = params =>
!params
? null
: map(params, name => <Fragment key=name>name </Fragment>);
render = () => (
<div>
<h2>Topics</h2>
<Links match=this.props.match />
<h2>
Topic ID param from Topic<strong>s</strong> Components
</h2>
<h3>this.state.params && this.showParams(this.state.params)</h3>
<NestedRoutes
handleUrlChange=this.handleUrlChange
match=this.props.match
paths=this.state.paths
showParams=this.showParams
/>
</div>
);
NestedRoutes.js
import map from "lodash/map";
import React, Fragment from "react";
import Route from "react-router-dom";
import Topic from "./Topic";
export default ( handleUrlChange, match, paths, showParams ) => (
<Fragment>
map(paths, path => (
<Route
exact
key=path
path=path
render=props => (
<Topic
...props
handleUrlChange=handleUrlChange
showParams=showParams
/>
)
/>
))
<Route
exact
path=match.url
render=() => <h3>Please select a topic.</h3>
/>
</Fragment>
);
【讨论】:
我考虑过这种方法,但它看起来很老套,因为我们应该能够在此处使用 URL 路径参数。这是一个简单直接的用例,几乎是一个标准用例 IMO 似乎不必在此处使用查询参数。话虽如此,我会将其添加到问题中的案例中 对我来说,这样的事情似乎太笼统了,无法与之匹配:path: "/orgs/:orgId/projects/:projectId/version/:version/models/:modelId"
。为什么有 4 个未知的 params
,实际上,您实际上是在处理这个 URL(在后端):/orgs?orgsId=orgsId&projects=projectsId&version=versionId&models=$modelsId
。此外,您希望/需要在什么时候限制 URL 参数(尤其是在您使用面包屑时):/company/companyname/invoices/browse/index/orderby/amount/sortby/date/desc/limit/50
。处理所有情况以匹配单个组件似乎是一场噩梦。
匹配到根 URL 更有意义,然后单独处理查询。例如,您匹配到:/company/:params
,然后您可以添加 1、10、20...任意数量的查询,而无需在您的 Route
的 path
中处理它们。同样容易的是,您可以删除查询,而无需导航到另一个组件。
除了提供静态文件夹式结构的网站(如 Github)之外,您尝试实现的路由还有其他优势或目的吗?【参考方案3】:
如果你有一组已知的子路由,那么你可以使用这样的东西:
Import BrowserRouter as Router, Route, Switch from 'react-router-dom'
<Router>
<Route path=`$baseUrl/home/:expectedTag?/:expectedEvent?` component=Parent />
</Router>
const Parent = (props) =>
return (
<div >
<Switch>
<Route path=`$baseUrl/home/summary` component=ChildOne />
<Route
path=`$baseUrl/home/:activeTag/:activeEvent?/:activeIndex?`
component=ChildTwo
/>
</Switch>
<div>
)
上例中Parent会获取expectedTag,expectedEvent作为匹配参数,与子组件没有冲突,子组件会获取activeTag,activeEvent,activeIndex作为参数。 params也可以使用同名,我也试过了。
【讨论】:
这当然是最简单的方法,一个可行的路由示例:TEST_PARENT_ROUTE = '/parent/:parentId/children1/:childId1/:children2?/:childId2?/:children3?/:childId3 ?' 谢谢@xtrinch,这是我的第一个答案,任何关于如何改进社区支持的 cmets 或帮助。【参考方案4】:尝试做这样的事情:
<Switch>
<Route path="/auth/login/:token" render=props => <Login ...this.props ...props/>/>
<Route path="/auth/login" component=Login/>
先是带参数的路由,后是不带参数的链接。
在我的登录组件中,我将这行代码console.log(props.match.params.token);
用于测试并为我工作。
【讨论】:
【参考方案5】:如果你碰巧使用 React.FC,有一个钩子useRouteMatch
。
比如父组件路由:
<div className="office-wrapper">
<div className="some-parent-stuff">
...
</div>
<div className="child-routes-wrapper">
<Switch>
<Route exact path=`/office` component=List />
<Route exact path=`/office/:id` component=Alter />
</Switch>
</div>
</div>
在您的子组件中:
...
import useRouteMatch from "react-router-dom"
...
export const Alter = (props) =>
const match = useRouteMatch()
const officeId = +match.params.id
//... rest function code
【讨论】:
以上是关于React Router v4 嵌套匹配参数无法在根级别访问的主要内容,如果未能解决你的问题,请参考以下文章
使用 React Router v4 的嵌套路由 - 解决方案