React Router v4 嵌套匹配参数在根级别无法访问

IT技术 javascript reactjs routing nested react-router
2021-04-03 14:35:06

测试用例

https://codesandbox.io/s/rr00y9w2wm

重现步骤

或者

预期行为

  • match.params.topicId应该与父主题组件相同 应该与match.params.topicId主题组件中访问时相同

实际行为

  • match.params.topicIdTopic组件中访问时未定义
  • match.params.topicId主题组件中访问时正在呈现

我从这个封闭的问题中了解到,这不一定是一个错误。

此要求在想要在工厂 Web 应用程序中创建运行的用户中非常普遍,其中Topics父级组件需要访问match.params.paramId其中paramId是匹配嵌套(子)组件的 URL 参数Topic

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} /> 路由/组件。

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

由于位于应用程序根级别的导航栏组件无法使用sectionIdorsections路径,因此有必要为这种常见用例编写这样的hack

我在 React Router 方面根本不是专家,所以如果有人可以为这个用例冒险一个适当的优雅解决方案,我会认为这是一项富有成效的努力。优雅,我的意思是

  • 使用match而不是history.location.pathname
  • 不涉及像手动解析 window.location.xxx
  • 不使用 this.props.location.pathname
  • 不使用第三方库,如 path-to-regexp
  • 不使用查询参数

其他黑客/部分解决方案/相关问题:

  1. React Router v4 - 如何获取当前路由?

  2. React Router v4 全局与嵌套路由子项不匹配

蒂亚!

5个回答

React-router不会为您提供任何匹配的子 Route 的匹配参数,而是根据当前匹配为您提供参数。因此,如果您的路线设置如下

<Route path='/topic' component={Topics} />

Topics组件中你有一个 Route 像

<Route path=`${match.url}/:topicId` component={Topic} />

现在,如果您的 url/topic/topic1与内部 Route 匹配,但对于 Topics 组件,则匹配的 Route 仍然存在,/topic因此其中没有参数,这是有道理的。

如果您想获取主题组件中匹配的子路由的参数,则需要使用matchPathReact-router 提供实用程序并针对要获取其参数的子路由进行测试

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

参数提供者.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")
);

工作演示

@nikjohn 一个干燥友好的解决方案是使用上下文,我将在一段时间内更新我的答案以添加基于上下文的解决方案
2021-05-25 14:35:06
我最终做了类似的事情,所以我奖励你。郑重声明,我仍然认为 React Router 4 方法不适合一些常见的用例,这让我感到沮丧。我希望有人想出一种替代方法,比如 React Little Router(formidable.com/blog/2016/07/25/...
2021-06-05 14:35:06
@nikjohn,我同意 react-router-v4 并非最适合所有用例,我们必须进行大量调整才能获得结果。上述解决方案就是其中之一
2021-06-08 14:35:06
@nikjohn,为使用上下文的 DRY 友好方法添加了一个演示,请看一下。它可能有一些错误,但我认为在此基础上进行开发会对您有所帮助
2021-06-11 14:35:06
谢谢你的回答,舒巴姆。但这似乎不是很友好,考虑到我需要创建每条路径两次,一次在路线上,然后在导航菜单上。我正在寻找 DRY 解决方案
2021-06-14 14:35:06

尝试利用查询参数?来允许父级和子级访问当前选定的topic. 不幸的是,您将需要使用moduleqs因为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

✔ 不涉及hacky方法

话题.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将 params 存储到state并且子代state在其 params 更改时更新父代的

URL 的结构类似于文件夹树: /topics/rendering/optimization/pure-components/shouldComponentUpdate

工作示例:https : //codesandbox.io/s/9joknpm9jy

✔ 使用匹配来处理路由 URL

✔ 不使用 this.props.location.pathname

✔ 使用 lodash 进行对象到对象的比较

✔ 不涉及hacky方法

话题.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>
  );
}

嵌套路由.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 匹配更有意义,然后分别处理查询。例如,您匹配到: /company/:params,然后您可以添加 1、10、20... 任意数量的查询,而无需在Route's 中处理它们path同样容易的是,您可以删除查询而无需导航到另一个组件。
2021-05-22 14:35:06
我考虑过这种方法,但它看起来很糟糕,因为我们应该能够在这里使用 URL 路径参数。这是一个简单直接的用例,它几乎是一个标准用例
2021-06-03 14:35:06
似乎我们不必在 IMO 中使用查询参数。话虽如此,我会将其添加到问题中的案例中
2021-06-04 14:35:06
除了提供静态文件夹式结构的网站(如 Github)之外,您尝试实现的路由还有其他优势或目的吗?
2021-06-04 14:35:06
对我来说,像这样的东西似乎太笼统了,无法与:path: "/orgs/:orgId/projects/:projectId/version/:version/models/:modelId". 为什么有 4 unknown params,实际上,您实际上是在处理此 URL(在后端):/orgs?orgsId={orgsId}&projects={projectsId}&version={versionId}&models=${modelsId}此外,在什么时候您想要/需要限制 URL 参数(特别是如果您正在做面包屑):/company/companyname/invoices/browse/index/orderby/amount/sortby/date/desc/limit/50. 处理所有情况只是为了匹配单个组件似乎是一场噩梦。
2021-06-05 14:35:06

如果你有一组已知的子路由,那么你可以使用这样的东西:

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?'
2021-05-22 14:35:06
谢谢@xtrinch,这是我的第一个答案,关于如何改进社区支持的任何评论或帮助。
2021-06-20 14:35:06

尝试做这样的事情:

<Switch>
  <Route path="/auth/login/:token" render={props => <Login  {...this.props} {...props}/>}/>
  <Route path="/auth/login" component={Login}/>

首先是带参数的路由和不带参数的链接。在我的登录组件中,我将这行代码console.log(props.match.params.token);用于测试并为我工作。

如果您碰巧使用 React.FC,则有一个 hook 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
}