Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use Apollo Client + React Router to implement private routes and redirection based on user status?

I am using React Router 4 for routing and Apollo Client for data fetching & caching. I need to implement a PrivateRoute and redirection solution based on the following criteria:

  1. The pages a user is permitted to see are based on their user status, which can be fetched from the server, or read from the cache. The user status is essentially a set of flags we use to understand where the user is in our funnel. Example flags: isLoggedIn, isOnboarded, isWaitlisted etc.

  2. No page should even begin to render if the user's status does not permit them to be on that page. For example, if you aren't isWaitlisted, you are not supposed to see the waitlist page. When users accidentally find themselves on these pages, they should be redirected to a page that is suitable for their status.

  3. The redirection should also be dynamic. For example, say you try to view your user profile before you are isLoggedIn. Then we need to redirect you to the login page. However, if you are isLoggedIn but not isOnboarded, we still don't want you to see your profile. So we want to redirect you to the onboarding page.

  4. All of this needs to happen on the route level. The pages themselves should be kept unaware of these permissions & redirections.

In conclusion, we need a library that given the user status data, can

  • compute whether a user can be on a certain page
  • compute where they need to be redirected to dynamically
  • do these before rendering any page
  • do these on the route level

I'm already working on a general-use library, but it has its shortcomings right now. I'm seeking opinions on how one should approach this problem, and whether there are established patterns to achieve this goal.

Here is my current approach. This is not working because the data the getRedirectPath needs is in the OnboardingPage component.

Also, I can't wrap the PrivateRoute with the HOC that could inject the props required to compute the redirect path because that would not let me use it as a child of the Switch React Router component as it stops being a Route.

<PrivateRoute
  exact
  path="/onboarding"
  isRender={(props) => {
    return props.userStatus.isLoggedIn && props.userStatus.isWaitlistApproved;
  }}
  getRedirectPath={(props) => {
    if (!props.userStatus.isLoggedIn) return '/login';
    if (!props.userStatus.isWaitlistApproved) return '/waitlist';
  }}
  component={OnboardingPage}
/>
like image 476
AnApprentice Avatar asked Feb 08 '18 18:02

AnApprentice


People also ask

What is the best way to redirect a page using react router?

The useHistory() hook is first imported and then assigned to a variable, which is subsequently utilized in a button (for example) to redirect users once a specific action is taken. Using the onClick event, we can then call the . push() method to tell React Router where we want the button to redirect to.

What are two ways of handling redirect with react router?

Two ways to handle redirecting on a user event such as create, update and delete with React Router.

What can I use instead of redirect in react?

Redirecting in react-router-dom v6 is done with the <Navigate> Component with replace props. Save this answer.

Is redirect available in react router dom v6?

Handling Redirects in React Router v6 It is simply this: if you need to redirect, do it on the server before you render any React and send the right status code. That's it. If you do this, you'll get: better SEO for redirected URLs and.


2 Answers

General Approach

I would create an HOC to handle this logic for all of your pages.

// privateRoute is a function...
const privateRoute = ({
  // ...that takes optional boolean parameters...
  requireLoggedIn = false,
  requireOnboarded = false,
  requireWaitlisted = false
// ...and returns a function that takes a component...
} = {}) => WrappedComponent => {
  class Private extends Component {
    componentDidMount() {
      // redirect logic
    }

    render() {
      if (
        (requireLoggedIn && /* user isn't logged in */) ||
        (requireOnboarded && /* user isn't onboarded */) ||
        (requireWaitlisted && /* user isn't waitlisted */) 
      ) {
        return null
      }

      return (
        <WrappedComponent {...this.props} />
      )
    }
  }

  Private.displayName = `Private(${
    WrappedComponent.displayName ||
    WrappedComponent.name ||
    'Component'
  })`

  hoistNonReactStatics(Private, WrappedComponent)

  // ...and returns a new component wrapping the parameter component
  return Private
}

export default privateRoute

Then you only need to change the way you export your routes:

export default privateRoute({ requireLoggedIn: true })(MyRoute);

and you can use that route the same way you do today in react-router:

<Route path="/" component={MyPrivateRoute} />

Redirect Logic

How you set this part up depends on a couple factors:

  1. How you determine whether a user is logged in, onboarded, waitlisted, etc.
  2. Which component you want to be responsible for where to redirect to.

Handling user status

Since you're using Apollo, you'll probably just want to use graphql to grab that data in your HOC:

return graphql(gql`
  query ...
`)(Private)

Then you can modify the Private component to grab those props:

class Private extends Component {
  componentDidMount() {
    const {
      userStatus: {
        isLoggedIn,
        isOnboarded,
        isWaitlisted
      }
    } = this.props

    if (requireLoggedIn && !isLoggedIn) {
      // redirect somewhere
    } else if (requireOnboarded && !isOnboarded) {
      // redirect somewhere else
    } else if (requireWaitlisted && !isWaitlisted) {
      // redirect to yet another location
    }
  }

  render() {
    const {
      userStatus: {
        isLoggedIn,
        isOnboarded,
        isWaitlisted
      },
      ...passThroughProps
    } = this.props

    if (
      (requireLoggedIn && !isLoggedIn) ||
      (requireOnboarded && !isOnboarded) ||
      (requireWaitlisted && !isWaitlisted) 
    ) {
      return null
    }

    return (
      <WrappedComponent {...passThroughProps} />
    )
  }
}

Where to redirect

There are a few different places you can handle this.

Easy way: routes are static

If a user is not logged in, you always want to route to /login?return=${currentRoute}.

In this case, you can just hard code those routes in your componentDidMount. Done.

The component is responsible

If you want your MyRoute component to determine the path, you can just add some extra parameters to your privateRoute function, then pass them in when you export MyRoute.

const privateRoute = ({
  requireLoggedIn = false,
  pathIfNotLoggedIn = '/a/sensible/default',
  // ...
}) // ...

Then, if you want to override the default path, you change your export to:

export default privateRoute({ 
  requireLoggedIn: true, 
  pathIfNotLoggedIn: '/a/specific/page'
})(MyRoute)

The route is responsible

If you want to be able to pass in the path from the routing, you'll want to receive props for these in Private

class Private extends Component {
  componentDidMount() {
    const {
      userStatus: {
        isLoggedIn,
        isOnboarded,
        isWaitlisted
      },
      pathIfNotLoggedIn,
      pathIfNotOnboarded,
      pathIfNotWaitlisted
    } = this.props

    if (requireLoggedIn && !isLoggedIn) {
      // redirect to `pathIfNotLoggedIn`
    } else if (requireOnboarded && !isOnboarded) {
      // redirect to `pathIfNotOnboarded`
    } else if (requireWaitlisted && !isWaitlisted) {
      // redirect to `pathIfNotWaitlisted`
    }
  }

  render() {
    const {
      userStatus: {
        isLoggedIn,
        isOnboarded,
        isWaitlisted
      },
      // we don't care about these for rendering, but we don't want to pass them to WrappedComponent
      pathIfNotLoggedIn,
      pathIfNotOnboarded,
      pathIfNotWaitlisted,
      ...passThroughProps
    } = this.props

    if (
      (requireLoggedIn && !isLoggedIn) ||
      (requireOnboarded && !isOnboarded) ||
      (requireWaitlisted && !isWaitlisted) 
    ) {
      return null
    }

    return (
      <WrappedComponent {...passThroughProps} />
    )
  }
}

Private.propTypes = {
  pathIfNotLoggedIn: PropTypes.string
}

Private.defaultProps = {
  pathIfNotLoggedIn: '/a/sensible/default'
}

Then your route can be rewritten to:

<Route path="/" render={props => <MyPrivateComponent {...props} pathIfNotLoggedIn="/a/specific/path" />} />

Combine options 2 & 3

(This is the approach that I like to use)

You can also let the component and the route choose who is responsible. You just need to add the privateRoute params for paths like we did for letting the component decide. Then use those values as your defaultProps as we did when the route was responsible.

This gives you the flexibility of deciding as you go. Just note that passing routes as props will take precedence over passing from the component into the HOC.

All together now

Here's a snippet combining all the concepts from above for a final take on the HOC:

const privateRoute = ({
  requireLoggedIn = false,
  requireOnboarded = false,
  requireWaitlisted = false,
  pathIfNotLoggedIn = '/login',
  pathIfNotOnboarded = '/onboarding',
  pathIfNotWaitlisted = '/waitlist'
} = {}) => WrappedComponent => {
  class Private extends Component {
    componentDidMount() {
      const {
        userStatus: {
          isLoggedIn,
          isOnboarded,
          isWaitlisted
        },
        pathIfNotLoggedIn,
        pathIfNotOnboarded,
        pathIfNotWaitlisted
      } = this.props

      if (requireLoggedIn && !isLoggedIn) {
        // redirect to `pathIfNotLoggedIn`
      } else if (requireOnboarded && !isOnboarded) {
        // redirect to `pathIfNotOnboarded`
      } else if (requireWaitlisted && !isWaitlisted) {
        // redirect to `pathIfNotWaitlisted`
      }
    }

    render() {
      const {
        userStatus: {
          isLoggedIn,
          isOnboarded,
          isWaitlisted
        },
        pathIfNotLoggedIn,
        pathIfNotOnboarded,
        pathIfNotWaitlisted,
        ...passThroughProps
      } = this.props

      if (
        (requireLoggedIn && !isLoggedIn) ||
        (requireOnboarded && !isOnboarded) ||
        (requireWaitlisted && !isWaitlisted) 
      ) {
        return null
      }
    
      return (
        <WrappedComponent {...passThroughProps} />
      )
    }
  }

  Private.propTypes = {
    pathIfNotLoggedIn: PropTypes.string,
    pathIfNotOnboarded: PropTypes.string,
    pathIfNotWaitlisted: PropTypes.string
  }

  Private.defaultProps = {
    pathIfNotLoggedIn,
    pathIfNotOnboarded,
    pathIfNotWaitlisted
  }
  
  Private.displayName = `Private(${
    WrappedComponent.displayName ||
    WrappedComponent.name ||
    'Component'
  })`

  hoistNonReactStatics(Private, WrappedComponent)
  
  return graphql(gql`
    query ...
  `)(Private)
}

export default privateRoute

I'm using hoist-non-react-statics as suggested in the official documentation.

like image 141
Luke Willis Avatar answered Oct 07 '22 02:10

Luke Willis


I personnaly use to build my private routes like this :

const renderMergedProps = (component, ...rest) => {
  const finalProps = Object.assign({}, ...rest);
  return React.createElement(component, finalProps);
};

const PrivateRoute = ({
  component, redirectTo, path, ...rest
}) => (
  <Route
    {...rest}
    render={routeProps =>
      (loggedIn() ? (
        renderMergedProps(component, routeProps, rest)
      ) : (
        <Redirect to={redirectTo} from={path} />
      ))
    }
  />
);

In this case, loggedIn() is a simple function that return true if user is logged (depends on how you handle the user session), you can create each of your private route like this.

Then you can use it in a Switch :

<Switch>
    <Route path="/login" name="Login" component={Login} />
    <PrivateRoute
       path="/"
       name="Home"
       component={App}
       redirectTo="/login"
     />
</Switch>

All subRoutes from this PrivateRoute will first need to check if user is logged in.

Last step is to nest your routes according to their required status.

like image 38
Dyo Avatar answered Oct 07 '22 02:10

Dyo