'React Routes problem with Tags other than Route

Using "react-router-dom": "^5.3.2"

In my component I have the following code:

render() {
  return <div role="main">
    <Switch>
      <Route path="/auth" exact component={Auth} />
      <PrivateRoute path="/" exact component={() => <>Main page</>} />

      <IsGranted for={[ 'superuser' ]}>
        <PrivateRoute path="/servers" exact component={ServerList} />
      </IsGranted>
  
      {/* or even nested in a <></> */}

      <Route component={_ => <h1>Not found</h1>} />
    </Switch>
  </div>
}

const PrivateRoute = ({ component:Component, ...rest }) => 
  <Route {...rest} render={props => isAuthenticated() ? <Component {...props}/> : <Redirect to={{ pathname:'/auth', state:{ from:props.location } }}/> }/>

const IfGranted = ({ children }) => condition() ? children : null 

The problem is, everything below the empty tag <> is ignored.

For example, I expect Not found be rendered, if I type in some gibberish in the URL. That happens only, if the <>...</> is commented out.

Is there anything I can do here?



Solution 1:[1]

The only valid children of the Switch component are the Route component (including custom route components) and the Redirect component. React.Fragment breaks this abstraction. The Switch component returns the first child <Route> or <Redirect> that matches the location or in the case where a child component is neither, that child is returned and nothing below it is reachable, i.e. the "Not Found" route.

From what I see of your snippet there is no need for the React.Fragment wrapping the PrivateRoute component.

  1. Remove the extraneous fragment around the "/servers" route.
  2. Use the route's component prop for React component references, use the render prop for inline functions.
  3. Order the routes in inverse order by route path specificity, this removes the need to specify the exact prop on every route. Since you have a "not found" catch all route, the home path "/" above it will need the exact prop to preclude it from all other generic matching.

Code

render() {
  return <div role="main">
    <Switch>
      <Route path="/auth" component={Auth} />
      <PrivateRoute path="/servers" component={ServerList} />
      <PrivateRoute exact path="/" render={() => <>Main page</>} />
      <Route render={() => <h1>Not found</h1>} />
    </Switch>
  </div>
}

PrivateRoute

To make the PrivateRoute align more with the Route component API it should take all the same props as a Route and render either a Route or Redirect component. Use the useLocation hook to access the current location object for redirect purposes.

Example:

const PrivateRoute = props => {
  const location = useLocation();

  return isAuthenticated()
    ? <Route {...props} />
    : (
      <Redirect
        to={{
          pathname:'/auth',
          state: { from: location }
        }}
      />
    );
};

Update

To add the ability to use roles to restrict access I would either wrap the routed component directly, or create a Higher Order component, or abstract another PrivateRolesRoute component.

  • Using wrapper

     <Switch>
       <Route path="/auth" component={Auth} />
       <PrivateRoute
         path="/servers"
         render={props => (
           <IsGranted for={['superuser']}>
             <ServerList {...props} />
           </IsGranted>
         )}
       />
       <PrivateRoute exact path="/" render={() => <>Main page</>} />
       <Route render={() => <h1>Not found</h1>} />
     </Switch>
    
  • Using HOC

    Create a HOC that takes a component to decorate and a roles array, and returns a wrapped component.

     const withIsGranted = (Component, roles = []) => props => (
       <IsGranted for={roles}>
         <Component {...props} />
       </IsGranted>
     );
    

    Decorate the component you want to role protect.

     export default withIsGranted(ServerList, ['superuser']);
    

    Use the exported decorated component in the route. It won't look different than it was previously.

     <Switch>
       <Route path="/auth" component={Auth} />
       <PrivateRoute path="/servers" component={ServerList} />
       <PrivateRoute exact path="/" render={() => <>Main page</>} />
       <Route render={() => <h1>Not found</h1>} />
     </Switch>
    
  • Using PrivateRolesRoute route component

    Create a new private route component that takes additional props and handles the conditional rendering of the protected route.

     const PrivateRolesRoute = ({ roles = [], ...props }) => {
       const location = useLocation();
    
       return isAuthenticated()
         ? (
           <IsGranted for={roles}>
             <Route {...props} />
           </IsGranted>
         )
         : (
           <Redirect
             to={{
               pathname:'/auth',
               state: { from: location }
             }}
           />
         );
     };
    

    Use the new PrivateRoleRoute and pass the roles prop.

     <Switch>
       <Route path="/auth" component={Auth} />
       <PrivateRoleRoute
         path="/servers"
         component={ServerList}
         roles={['superuser']}
       />
       <PrivateRoute exact path="/" render={() => <>Main page</>} />
       <Route render={() => <h1>Not found</h1>} />
     </Switch>
    

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1