【问题标题】:React Router v4 Nested match params not accessible at root levelReact Router v4 嵌套匹配参数无法在根级别访问
【发布时间】:2019-03-12 01:08:15
【问题描述】:

测试用例

https://codesandbox.io/s/rr00y9w2wm

重现步骤

预期行为

  • 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
  • 不使用查询参数

其他技巧/部分解决方案/相关问题:

  1. React Router v4 - How to get current route?

  2. React Router v4 global no match to nested route childs

TIA!

【问题讨论】:

    标签: javascript reactjs routing nested react-router


    【解决方案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
      }
      

      【讨论】:

        猜你喜欢
        • 2018-02-28
        • 1970-01-01
        • 1970-01-01
        • 2019-01-07
        • 1970-01-01
        • 1970-01-01
        • 2017-09-03
        • 2017-11-11
        相关资源
        最近更新 更多