Reputation: 21850
https://codesandbox.io/s/rr00y9w2wm
OR
match.params.topicId
should be identical from both the parent Topics component should be the same as match.params.topicId
when accessed within the Topic componentmatch.params.topicId
when accessed within the Topic component is undefinedmatch.params.topicId
when accessed within the Topics component is renderingI understand from this closed issue that this is not necessarily a bug.
This requirement is super common among users who want to create a run in the mill web application where a component Topics
at a parent level needs to access the match.params.paramId where paramId
is a URL param that matches a nested (child) component 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>
);
In a generic sense, Topics
could be a Drawer or Navigation Menu component and Topic
could be any child component, like it is in the application I'm developing. The child component has it's own :topicId
param which has it's own (let's say) <Route path="sections/:sectionId" component={Section} />
Route/Component.
Even more painful, the Navigation Menu needn't have a one-to-one relationship with the component tree. Sometimes the items at the root level of the menu (say Topics
, Sections
etc.) might correspond to a nested structure (Sections
is only rendered under a Topic, /topics/:topicId/sections/:sectionId
though it has its own normalized list that is available to the user under the title Sections in the Navigation Bar).
Therefore, when Sections is clicked, it should be highlighted, and not both Sections and Topics.
With the sectionId
or sections
path unavailable to the Navigation Bar component which is at the Root level of the application, it becomes necessary to write hacks like this for such a commonplace use case.
I am not an expert at all at React Router, so if anyone can venture a proper elegant solution to this use case, I would consider this to be a fruitful endeavor. And by elegant, I mean
match
and not history.location.pathname
window.location.xxx
this.props.location.pathname
path-to-regexp
Other hacks/partial solutions/related questions:
TIA!
Upvotes: 14
Views: 14477
Reputation: 215
If you happen to use React.FC, there is a hook useRouteMatch
.
For instance, parent component routes:
<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>
And in your child component:
...
import { useRouteMatch } from "react-router-dom"
...
export const Alter = (props) => {
const match = useRouteMatch()
const officeId = +match.params.id
//... rest function code
}
Upvotes: 0
Reputation: 91
If you have a known set of child routes then you can use something like this:
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>
)
}
In the above example Parent will get expectedTag, expectedEvent as the match params and there is no conflict with the child components and Child component will get activeTag, activeEvent, activeIndex as the parameters. Same name for params can also be used, I have tried that as well.
Upvotes: 1
Reputation: 29
Try to do something like this:
<Switch>
<Route path="/auth/login/:token" render={props => <Login {...this.props} {...props}/>}/>
<Route path="/auth/login" component={Login}/>
First, the route with the parameter and after the link without parameter.
Inside my Login component I put this line of code console.log(props.match.params.token);
to test and worked for me.
Upvotes: 0
Reputation: 19762
Try utilizing query parameters ?
to allow the parent and child to access the current selected topic
. Unfortunately, you will need to use the module qs because react-router-dom
doesn't automatically parse queries (react-router v3 does).
Working example: https://codesandbox.io/s/my1ljx40r9
URL is structured like a concatenated string:
topic?topic=props-v-state
Then you would add to the query with &
:
/topics/topic?topic=optimization&category=pure-components&subcategory=shouldComponentUpdate
✔ Uses match for Route URL handling
✔ Doesn't use this.props.location.pathname
(uses this.props.location.search
)
✔ Uses qs
to parse location.search
✔ Does not involve hacky approaches
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>
);
};
Another approach would be to create a HOC
that stores params to state
and children update the parent's state
when its params have changed.
URL is structured like a folder tree: /topics/rendering/optimization/pure-components/shouldComponentUpdate
Working example: https://codesandbox.io/s/9joknpm9jy
✔ Uses match for Route URL handling
✔ Doesn't use this.props.location.pathname
✔ Uses lodash for object to object comparison
✔ Does not involve hacky approaches
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>
);
Upvotes: 8
Reputation: 281734
React-router
doesn't give you the match params of any of the matched children Route , rather it gives you the params based on the current match. So if you have your Routes setup like
<Route path='/topic' component={Topics} />
and in Topics
component you have a Route like
<Route path=`${match.url}/:topicId` component={Topic} />
Now if your url is /topic/topic1
which matched the inner Route but for the Topics component, the matched Route is still, /topic
and hence has no params in it, which makes sense.
If you want to fetch params of the children Route matched in the topics component, you would need to make use of matchPath
utility provided by React-router and test against the child route whose params you want to obtain
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>
)
}
EDIT:
One method to get all the params at any level is to make use of context and update the params as and when they match in the context Provider.
You would need to create a wrapper around Route for it to work correctly, A typical example would look like
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>
);
}
}
Index.js
ReactDOM.render(
<BrowserRouter>
<ParamsProvider>
<App />
</ParamsProvider>
</BrowserRouter>,
document.getElementById("root")
);
Upvotes: 14