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.topicIdTopic 组件中访问时为 undefined match.params.topicIdTopics 组件中访问时是 rendering

我从this closed issue 了解到,这不一定是错误。

此要求在想要在工厂 Web 应用程序中创建运行的用户中非常普遍,其中父级别的组件 Topics 需要访问 ma​​tch.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 参数,它有它自己的(比如说)&lt;Route path="sections/:sectionId" component=Section /&gt; Route/Component。

更痛苦的是,导航菜单不必与组件树建立一对一的关系。有时菜单根级别的项目(例如TopicsSections 等)可能对应于 嵌套 结构(Sections 仅在主题下呈现,/topics/:topicId/sections/:sectionId 虽然它有自己的规范化列表,用户可以在导航栏中的标题 Sections 下使用)。 因此,当 Sections 被点击时,it 应该被突出显示,而不是 Sections Topics。

由于位于应用程序根级别的导航栏组件无法使用sectionIdsections 路径,因此有必要为这种常见的用例编写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

然后您将使用&amp; 添加到查询中:

/topics/topic?topic=optimization&amp;category=pure-components&amp;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&amp;projects=projectsId&amp;version=versionId&amp;models=$modelsId。此外,您希望/需要在什么时候限制 URL 参数(尤其是在您使用面包屑时):/company/companyname/invoices/browse/index/orderby/amount/sortby/date/desc/limit/50。处理所有情况以匹配单个组件似乎是一场噩梦。 匹配到根 URL 更有意义,然后单独处理查询。例如,您匹配到:/company/:params,然后您可以添加 1、10、20...任意数量的查询,而无需在您的 Routepath 中处理它们。同样容易的是,您可以删除查询,而无需导航到另一个组件。 除了提供静态文件夹式结构的网站(如 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 的嵌套路由 - 解决方案

在 react-router v4 中嵌套路由

浅入浅出 react-router v4 实现嵌套路由

React Router v4 嵌套路由 props.children

React Router v4 中的嵌套路由

React-router-dom v4 嵌套路由不起作用