React-router: Delegate route matching logic from <Switch> to <Route> (feature request)

Created on 21 Sep 2017  路  8Comments  路  Source: ReactTraining/react-router

Hello,

currently you have following logic in <Switch> component:

https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Switch.js#L41-L60

<Switch> component makes assumption, that it will always receive the 'whatever' component, but with the specific options (path, exact, etc).

What the bad here is that if you'll want to wrap <Route> component in HOC with a different props interface, this new <HocRoute> won't work with <Switch> anymore, because <Switch> has its own copy-pasted matching logic and it, actually routes and only then switches.

What I want to propose is to delegate routing logic to <Route> components only. E.g. by creating static computeMatch() method and by calling element.type.computeMatch(props, router) instead of calling matchPath() on props, that do not belong to <Switch> component.

I know, that element.type is a little bit hacky, but this will allow a great composability and extendability of react-router.

The use cases

Named routes

You can implement something like this:

<NamedRoute name="home" component={HomePage} />
import { Route } from 'react-router-dom';

const routes = {
  home: '/'
};

class NamedRoute extends Component // or it can be extends Route too {
  static computeMatch({ name, ...rest }, router) {
    const path = routes[name] || '/';

    return Route.computeMatch({ path, ...rest }, router);
  }

  render() {
    return <Route {...this.props} />;
  }
}

Route aliasing

<Route aliases={['/', '/index']} component={HomePage} />
import { Route, Switch } from 'react-router-dom';

class AliasRoute extends Component {
  static computeMatch({ aliases, ...rest }, router) {
    // btw. the alias can be the whole object
    // with props for Route instead of only path
    return aliases.reduce((match, path) => {
      if (match) {
        return match;
      }

      return Route.computeMatch({ path, ...rest }, router);
    }, null);
  }

  render() {
    const { aliases, ...rest } = this.props;

    return (
      <Switch>
        {aliases.map((path) => (
          <Route path={path} {...rest} />
        ))}
      </Switch>
    );
  }
}

This component can actually solve another <Switch> nesting problem, but the actual <Switch> component is hidden inside <AliasRoute>.

Switch nesting

The <Switch> can actually implement computeMatch() too and allow nesting:

  static computeMatch({ children, location }, router) {
    let match = null;

    React.Children.forEach(children, (child) => {
      if (match) {
        return true;
      }

      match = child.type.computeMatch({location, ...child.props}, router);
    });

    return match;
  }
stale

Most helpful comment

I'm currently having problems with the same issue (having my own route component and not being able to say "don't use this route"). Having a SwitchableComponent#computeMatch override check looks best to me, due to its ability to look at the props of the component via this.props.

My use case is routes that are only available when a user has certain permissions. This is currently "not easy" to "very difficult" to specify with just components.

All 8 comments

I'm not sure I'm sold on this. Re-using a property of the component seems like a good idea, but it means we are depending on the type of child given to Switch. The dirty little secret of Switch right now is that this works:

<Switch>
  <SomeComponent path="/" />
</Switch>

We basically fail gracefully if you pass a "bad" child in.

I'm also pessimistic about a hidden API surface. This should be more out in the open. Perhaps could use a computeMatch prop that takes a function for computing the match. That is more explicit and visible.

@timdorr Yep, I've busted that secret today :D

We can keep that secret with my proposal too. You just need to check, whether the element actually has the method required. If no, then skip it.

The feature with rendering of <div>'s depending on path prop is not the expected behavior and I hope, no one will exploit it.

Perhaps could use a computeMatch prop

So you accept the idea of traversing and passing props to some function, but you want that function to be a prop too. Am I right? So instead of child.type.computeMatch we will call props.computeMatch, right?

It looks good too. But this will force the client code to provide computeMatch and thus, will delegate the whole routing logic to client code, because you won't check defaultProps of HOC (the only way to set props on HOC so that the client code won't be forced to pass them) and won't traverse to it's children (from the <Switch> point of view it is children of children to get the first needed child and there is no way to know when to stop).

You'll be able to do this:

<Route computeMatch={(...) => matchPath(...)} />

but you won't be able to do this:

function HocRoute({ computeMatch }) {
    return <Route computeMatch={computeMatch} />;
}

HocRoute.defaultProps = {
  computeMatch: (...) => matchPath(...)
};

The problem with <Switch>, that it tries abstract 'if-else' to remain declarative. It is not possible without hidden API (if you are examining/changing the props of children this is already a hidden api). If you'll try to make logic more explicit, you will delegate the major part of logic to client code.

I think that Switch can be an exception in order to make the client code more declarative. As for me static function in routable components looks more explicit, than routing logic hidden in Switch with implicit overwriting of props.computedMatch from Router.

I'm currently having problems with the same issue (having my own route component and not being able to say "don't use this route"). Having a SwitchableComponent#computeMatch override check looks best to me, due to its ability to look at the props of the component via this.props.

My use case is routes that are only available when a user has certain permissions. This is currently "not easy" to "very difficult" to specify with just components.

I am currently thinking about implementing a HashRoute which would match the #hash part at the end of an url. I can't use it in a Switch because it is restreined to a specific pattern and does the computing itself instead of letting Route decide whether they should render or not. (And render the first one that does not render null)

The solution in the main post of this page seems to be the best fit because it correctly embraces the principle of abstraction: the Switch doesn't know how a Route decides to be rendered or not, it asks it directly.

It's been a while now, any update on this ?

I'm encountering similar issue:

<Switch><Child path="/foo"/></Switch>
const Child = ({path}) => (
  <Route
    path={`${path}/:foo?`}
    render={
      ({match}) => { /* Stale match from switch */ }
    }/>)

Wouldn't checking here if path and exact are same before reusing computed match be an easy and safe fix?
https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Route.js#L18

I'm also having problem with wrapping <Route> with custom component inside <Switch>.
I need something like this to be done:

<Switch>
  <ExtendedRoute path="/route">
    <Route path="/extended/route">

But the only working solution is to call ExtendedRoute as function and return <Route> component from it, wich seems very ugly :cry:

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ryansobol picture ryansobol  路  3Comments

stnwk picture stnwk  路  3Comments

imWildCat picture imWildCat  路  3Comments

ackvf picture ackvf  路  3Comments

winkler1 picture winkler1  路  3Comments