Hi, I'm in the process of upgrading to react-router 4 from v2. I'm not sure if this is a bug or not, but I'm having a really hard time getting my Header component to show the correct route params object. It doesn't seem like the match.params
object is available globally like the params
object was in react-router 2 and I'm not sure how to make it available to sibling components.
App.jsx (wrapped with ConnectedRouter)
-> contains Layout.jsx
const Layout = function(props) {
return (
<div className="layout" style={layout.base}>
<Header inlineStyles={layout.header} params={props.match.params} />
<Content inlineStyles={layout.content} />
<Footer inlineStyles={layout.footer} />
</div>
);
};
Layout.propTypes = {
match: PropTypes.object
};
export default Layout;
The Content
component is where the bulk of my routes begin. When I try to access props.match
inside of the Header component, I see this:
// Header.jsx
match: {
isExact: false,
params: {},
path: "/",
url: "/"
}
But when I access the props.match
object from the nested component route, I get this:
// Content.jsx > Child > GrandChild
match: {
isExact: true,
params: {orgId: "1", projectId: "11", version: "1.0.0", modelId: "30"},
path: "/orgs/:orgId/projects/:projectId/version/:version/models/:modelId",
url: "/orgs/1/projects/11/version/1.0.0/models/30"
}
This is in the same render cycle.
match
prop is <Route>
relative (that is to say, inherited from the nearest parent <Route>
)// location = { pathname: '/album/123/song/456' }
// also, this is demonstrating the element tree, not the createElement statements
<BrowserRouter>
{/* the root router creates an "empty" match, so match.params = {} */}
<Route path='/album/:albumID' component={Album}>
{/* match.params = { albumID: '123' } */}
<Album>
{/* match.path = '/album/:albumID' */}
<Route path={`${match.path}/song/:songID`} component={Song}>
{/* match.params = { albumID: '123', songID: '456' } */}
<Song>
<Header>
and <Content>
components based on the matched route<Header>
to figure out which route matchescontext
"magic" to get route components to report their params.I don't actually know what you need the params in your header for, so I can't really give a recommendation on which approach is "best", but it's probably either a or b.
Also, if you have any further usage questions, please use StackOverflow or Reactiflux since those communities are setup to answer questions. The issues section here is meant for bug reports.
Thanks for the response, @pshrmn. I have a custom Breadcrumb component that needs access to the global set of route params in order to render properly (swapping out the param numbers for their respective names). Having the params be relative is really counter-intuitive, especially since the location URL is available globally.
To me, it is not clear why you would ever Not include all params in the match object, I really do not see a use case where you would not want all params to be listed (opposite argument). Because as mentioned by @goodbomb, you can always read the location URL in full.
I could give you an example for my use case, it involves layouts at the root of the Router, with dynamic and "static-ish" content, that still needs to change based on the route currently viewed. (And I now have to write some URL parsing algorithm).
I would consider it a total anti-pattern to ever have two matching ids with the same name like /document/:docId/nested/:docId/nested2/:docId
, where I can understand that you don't want them to be overridden. Adding a check for that in every path
would do the trick.
I believe merging all match objects is the most intuitive way to go.
Edit: So for me, this is a bug, and is in the right place :)
Thanks @Floriferous, I agree completely. So should this issue be re-opened? Will the match objects be merged in a future release?
I also find it totally counter-intuitive that match would not always show this. And I need it because I'm using a container component around my routes to log page views to a database. I wound up doing it with the container because I could find no good place to do it anywhere in the page component itself (constructor would only log a page view the first time a page was loaded or reloaded from the server, and componentDidMount would log every time the page rerendered, giving, for example, three page view records when really the user only viewed the page once). But, with the container, I can do it on the initial load and then check for a pathname change on properties change, and it behaves exactly as I want that way.
What I'm left with is being forced to re-invent the wheel myself and write my own code to parse out the requested pathname in a rather clunky way that assumes certain segments are parameters, which both duplicates logic and opens up lots of great opportunities for bad page view data to be recorded.
@pshrmn Your response that this is an inappropriate place to post this suggests that user input requesting clarification on whether behavior is either expected or a bug, is unwelcome. It also seems to indicate being very closed to input on how things ought to behave, or what users of this library would like to see. I don't mean to be overly negative, especially because I love React Router and appreciate all the work on it. But this seems a little bit discouraging to the community of the library's users, and rather dismissive of user concerns.
I agree with @Floriferous, it seems counter intuitive that your params wouldn't be globally available. Would be very useful in a number of situations to have access to them.
Yes the params are global in the browser, the url can be considered a global right? So i makes sense that it would be available in any withRouter
component
I also ran into this problem. My use case is exactly the same as goodbomb's, I have Breadcrumbs
that are in the parent component with Route
s as children, so it isn't getting the match
I need.
Good to know this was intentional. I guess I'm not the only one who didn't pay close attention to the documentation:
A match object contains information about how a
<Route path>
matched the URL.
So I'm going to try matchPath
within my Breadcrumbs
component. Hope that helps others stumbling on this issue.
[
matchPath
] lets you use the same matching code thatuses except outside of the normal render cycle
Thank you @CaitlinWeb for the matchPath
suggestion, that's super helpful.
For anyone else running into this problem, this was my solution:
import { matchPath } from 'react-router'
const match = matchPath(this.props.history.location.pathname, {
path: '/path/:param',
exact: true,
strict: false
})
And then you can use the match
object just like you would normally:
let parameter = match.params.param
// do what you will the the param
@spiritman110 very helpful, saved me from apocalypse
Thanks @spiritman110, didn't know that API was exposed!
It's still not an acceptable solution though, since you have to provide the path you want to match, when what's asked here is that any matched param just matches everywhere. This is really inconvenient if you want it to work anywhere in a remotely scaleable manner
I wrote this completely over the top HOC that includes @spiritman110 suggestion.
import { withRouter, matchPath } from 'react-router-dom';
import isArray from 'lodash/isArray';
import { compose, mapProps } from 'recompose';
// Lets you pass a param as a string, or an array of params, and you will get
// them as simple props from react-router, instead of drilling down
// match.params.paramName
export default (paramName, path) =>
compose(
withRouter,
mapProps(({ match, history, location, ...rest }) => {
let realMatch;
if (path) {
realMatch = matchPath(history.location.pathname, {
path,
exact: false,
strict: false,
});
} else {
realMatch = match;
}
if (!realMatch) {
return { ...rest };
}
if (isArray(paramName)) {
return paramName.reduce(
(acc, param) => ({
...acc,
[param]: realMatch.params[param],
}),
{ ...rest },
);
}
return { [paramName]: realMatch.params[paramName], ...rest };
}),
);
Here's how to use it:
const MyComponent = ({ userId }) => <div />;
// If under the Route, to avoid all the unnecessary prop drilling
export default withMatchParam('userId')(MyComponent);
// If outside a Route, as is the case in this thread
export default withMatchParam('userId', '/users/:userId')(MyComponent);
Assuming all the routes with the :userId
matcher look like /user/userId23/whatever
.
This issue should be revisited.
Thank god for matchPath
.
This should be re-opened and fixed..
I'm new to React and can't understand nothing of those "thumbs up" solutions comments above.
It all seems way over-complicated for something so simple.
So I ended up using the DOM API URLSearchParams
?step=2
const queryParamsString = this.props.location.search.substring(1), // remove the "?" at the start
searchParams = new URLSearchParams( queryParamsString );
step = searchParams.get("step");
With all due respect to the collaborators, I find it a bit strange that this issue has been closed. I am facing the same issue as well, and I find it really surprising that there is no universally accepted Design Pattern for handling this situation. The matchParam
solution seems to be a hack at best. I really love React Router and it disappoints me that the issue was closed prematurely and without a proper solution. Because of this and because I am powerless to open this issue, I'm opening a new issue
Not sure whether this is helpful at all to people still seeing this issue, but thought we were having this issue and wrote in a work around but it turned out to be actually that we'd misspelt the word exact
as excat
in our <Route>
component by accident.
As soon as we spotted that and fixed it, params
was filled with the params we were passing.
Noticed that if <Route>
is written using the component prop
<Route path="/view/:postId" component={Single} />
, the params is available inside <Single>
's props.match vs
<Route path="/view/:postId" render={() => <Single {...this.props} /> />
.
I like what @spiritman110 has suggested, but I also noticed that since match.params is available in the state of <Route>
is it also ok to pass it down to the child component via props? Are there any downsides to doing it via props?
Guys, this seems way too hacky of an approach. What @Shaderpixel says, is actually true, but the solutions provided here, are too over-engineered, for something that should be a no-brainer, as having the params as a global configuration. React-router has some really weird idiosyncrasies.
In my case, It won't work, because I have structured my app a bit differently than you did, I also thing that @pshrmn answer is very good, but I am using a second set of routes already and I am not planning to change the folder structure for that..
I don't understand why you closed this issue. It is still happening.
The question on Stack Overflow is here: https://stackoverflow.com/questions/53997670/react-router-dom-url-gets-redirected-to-the-root-path . If someone can take a look, and help me out a little bit it would be great.
the same issue, I have dynamic routing and I can't pass the same string to mathPath
I faced this issue today. Fortunately I have only 4 routes that are dependent on params. The quick hack that i did was to run matchPath against my routes and take only the matching path's params or return {}. I then hooked it up to mapStateToProps.
// in utils file
export const routePaths = [
// list of all paths in the app
];
/*
returns the params found in the supplied path
isEmpty is from lodash
*/
export const getParams = (path) => {
// this works since i use hash history
path = path || window.location.hash.slice(1, window.location.hash.length);
const matchObj = routePaths.map(function(r) {
const p = matchPath(path, {path: r});
return p;
}).find(p => !isEmpty(p));
return isEmpty(matchObj) ? {} : (matchObj.params || {});
}
/// now in connect() in our component
function mapStateToProps(state) {
return {
params: getParams()
};
}
// now this.props.params works as usual.
This works only if you know the routes beforehand.
- Yes. The
match
prop is<Route>
relative (that is to say, inherited from the nearest parent<Route>
)
Can we at least get an _explanation_ for this design decision? A lot of people are rightfully reporting this as a bug (or a critical missing feature), but it has been closed and brushed off with a “Yes”. @pshrmn
@lazarljubenovic The routing in react-router is hierarchical.
Each <Route>
evaluates the current location based on it's own props and propagates the result down the tree via context.
(Simplified syntax for demonstration purposes)
<Route path="/foo">
{/* match = { path: "/foo", url: "/foo", params: undefined } */}
<Route path="/foo/:id">
{/* match = { path: "/foo/:id", url: "/foo/bar", params: { id: "foo" } */}
</Route>
</Route>
In the above example, the outer route has no knowledge of the inner route.
I don't know exactly why the decision was made, but I think it's to allow easy nested routing and a simple codebase. Supporting both nested (and dynamic) routing like that while also communicating the innermost routing info up the tree while keeping todays flexibility isn't exactly trivial.
This issue is still happening and cost me a week of coding.
ok i solved that problem
On Tue, Jul 16, 2019 at 1:55 PM Tyrique Daniel notifications@github.com
wrote:
This issue is still happening and cost me a week of coding.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/ReactTraining/react-router/issues/5870?email_source=notifications&email_token=AJAQFFJTH3DAPPA5GRSVXF3P7WAWXA5CNFSM4ELUFI62YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD2ADCMA#issuecomment-511717680,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AJAQFFO3IZOGYA6U2UKMHCTP7WAWXANCNFSM4ELUFI6Q
.
- Yes. The
match
prop is<Route>
relative (that is to say, inherited from the nearest parent<Route>
)Can we at least get an _explanation_ for this design decision? A lot of people are rightfully reporting this as a bug (or a critical missing feature), but it has been closed and brushed off with a “Yes”. @pshrmn
Echoing this request for explanation. This is adding lots of complications to our code and I'd love to know the reasoning behind it.
Most helpful comment
Thank you @CaitlinWeb for the
matchPath
suggestion, that's super helpful.For anyone else running into this problem, this was my solution:
And then you can use the
match
object just like you would normally: