React-router: match.params returns empty even when URL contains params

Created on 13 Jan 2018  ·  25Comments  ·  Source: ReactTraining/react-router

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.

  1. Is it expected that the match object is not global to the entire router?
  2. How do I access the same match object in my Header.jsx file as I have in my Content.jsx?

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:

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

All 25 comments

  1. Yes. The 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>
  1. You would have to either
    a) change up the structure of your components so that you are matching routes at a root level and then render the <Header> and <Content> components based on the matched route
    b) have a second set of routes inside of your <Header> to figure out which route matches
    c) use some context "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 Routes 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 that uses 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

My example query string is (for registration process):

?step=2

And in the Registration component:

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 to address this 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.

  1. 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
.

  1. 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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

nicolashery picture nicolashery  ·  3Comments

Waquo picture Waquo  ·  3Comments

imWildCat picture imWildCat  ·  3Comments

ryansobol picture ryansobol  ·  3Comments

ackvf picture ackvf  ·  3Comments