简体   繁体   中英

Can I use higher-order private route components inside a Switch in React Router?

I'm trying to set up a Router to use custom PrivateRoute components that are equivalent to the example given in the official react-router docs. The catch is that I also want to use a catch-all 404 route, so I think I need to use a Switch component too, as shown in this example from the docs.

There's a conflict here because the docs for Switch state that the children of a Switch component have to be Route components, and in this issue the team states that using non-route components inside a Switch is unsupported, even if it happens to work.

I've created higher-order routes and am currently using them inside a Switch and it seems to be working, but this won't pass code review at my company, since it's using an unsupported API that could break at any time. I want to know if there's a fully supported way to use higher-order routes together with a catch-all route.

I'm looking into trying to create a 404 route without needing to use a Switch , or else using regular Route components but wrapping the component passed to the Route in the authentication logic instead.

import { Router, Route, Switch } from 'react-router-dom';

// Array of authorization functions to be tested by private routes
const authCriteria = [
  // The next route must have been referred by a previous route
  props => defined(props.location.state.from),
];

// Seems to work as expected, but violates the supported use 
// of the Switch component
const AppRouter = () => {
  return (
    <Router>
      <Switch>
        <Route exact path='/' component={Store} />
        <PrivateRoute 
          exact
          path='/account' 
          component={Account}
          authCriteria={authCriteria}
        />
        <PrivateRoute 
          exact
          path='/shipping' 
          component={Shipping} 
          authCriteria={authCriteria}
        />
        <PrivateRoute 
          exact 
          path='/checkout' 
          component={Checkout} 
          authCriteria={authCriteria}
        />
        // Catch-all route means we need to use a Switch
        <Route render={() => (<h2>Not Found</h2>)} />
      </Switch>
    </Router>
  );
};

export default AppRouter;

Based on your question's links to react-router's github, it doesn't appear like you can use a HOC inside a Switch .

However, you can achieve something similar. Basically, you need two routers. The first Router routes all pre-authenticated routes, like logging in, new users, password forgot, etc. Since it's inside a Switch , it should render your second Router . Your second Router will contain all your authentication required routes. However, you need to include a conditional check to make sure the user is authenticated. If the user isn't authenticated, you can just return a Redirect back to the /login path. This works because of the first Switch . If the user goes to a url that isn't available to unauthenticated users, the user will always get redirected to the /login since they will hit the default condition in the first switch and the Redirect of the second component.

import { Router, Route, Switch } from 'react-router-dom';

const AppRouter = (props) => {
  return (
    <Router>
      <Switch>
        <Route exact path='/login' component={Login} />
        <Route exact path='/new-user' component={CreateAccount} />
        <Route render={props => <AuthenticationRouter {...props} />}
      </Switch>
    </Router>
  );
};

const AuthenticatedRouter = (props) => {
  if( props.userIsAuthenticated ) {
    return (
      <Router>
        <Switch>
          <Route exact path='/' component={Store} />
          <Route 
            exact
            path='/account' 
            component={Account}
          />
          <Route 
            exact
            path='/shipping' 
            component={Shipping} 
          />
          <Route 
            exact 
            path='/checkout' 
            component={Checkout} 
          />
          <Route render={() => (<h2>Not Found</h2>)} />
        </Switch>
      </Router>
    );
  }

  return (
    <Redirect
      to={{
        pathname: "/login",
        state: { from: props.location }
      }}
    />
  );
};

export default AppRouter;

Update: I changed my route authentication HoC to wrap the component rendered by the route, not the route itself. The first answer that was posted would probably be better if the authentication criteria were static, but the app I'm working on is an order placement workflow where access to different routes can depend on different factors, like having completed or come from other routes, so I needed to be able to pass arbitrary authentication criteria to each route.

Here's the code that shows the withAuth HoC I used to wrap each component and how it works with the router:

// Test every passed-in auth verification function.
const verifyAuth = (authCriteria, props) => {
  if (authCriteria.length === 0) return true;
  return authCriteria.every(criterion => criterion(props));
};

// Authentication HoC
const withAuth = ({
  authCriteria = [],
  redirectPath = '/',
} = {}) => Component => props => {
  const isAuthorized = verifyAuth(authCriteria, props);
  return (
    isAuthorized ? (
      <Component {...props} />
    ) : (
      <Redirect 
        to={{ 
          pathname: redirectPath, 
          state: { from: props.location },
        }} 
      />
    )
  );
};

// TODO: authenticate user
const validUser = _props => true; 
// The next route must have been referred by a previous route
const internalReferral = props => defined(props.location.state);

// The Store route has different authentication requirements
// than the other two routes 
const storeCriteria = [validUser];
const mainCriteria = [validUser, internalReferral];

const authRoute = withAuth({ authCriteria: mainCriteria });

const ProtectedRoutes = {
  Store: withAuth({ authCriteria: storeCriteria })(Store),
  Shipping: authRoute(Shipping),
  Checkout: authRoute(Checkout),
};

const AppRouter = () => {
  return (
    <Router>
      <Switch>
        <Route exact path='/' component={ProtectedRoutes.Store} />
        <Route exact path='/shipping' component={ProtectedRoutes.Shipping} />
        <Route exact path='/checkout' component={ProtectedRoutes.Checkout} />
        <Route render={() => (<h2>Not Found</h2>)} />
      </Switch>
    </Router>
  );
};


The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM