React Router v4 Nested match params not accessible at root level

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

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

Working DEMO