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

I use React Router 4 for routing and Apollo Client for data & caching. I need to implement PrivateRoute and redirect solution based on the following criteria:

  1. The pages that the user is allowed to view are based on their user status, which can be obtained from the server or read from the cache. User status is, in fact, a set of flags that we use to understand where the user is in our funnel. Example flags: isLoggedIn , isOnboarded , isWaitlisted , etc.

  2. No page should even begin to appear if the status of the user does not allow them to be on this page. For example, if you are not isWaitlisted , you should not see the waiting list page. When users accidentally appear on these pages, they should be redirected to a page corresponding to their status.

  3. Redirection should also be dynamic. For example, let's say you are trying to view your user profile before you isLoggedIn . Then we need to redirect you to the login page. However, if you are isLoggedIn but not isOnboarded , we still do not want you to see your profile. Therefore, we want to redirect you to the registration page.

  4. All this must happen at the route level. The pages themselves should be unaware of these permissions & reassignments.

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

  • calculate whether the user can be on a specific page
  • calculate where they should be redirected dynamically
  • do this before displaying any page
  • do it at route level

I am already working on a shared library, but now it has its drawbacks. I am looking for opinions on how to approach this problem, and whether there are established models for achieving this goal.

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

In addition, I cannot wrap a PrivateRoute HOC that could implement the details necessary to calculate the redirect path, because that would not allow me to use it as a child component of the Switch React Router component, since it ceases to be 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} /> 
+11
source share
4 answers

General approach

I would create a 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 just need to change the way you export your routes:

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

and you can use this route the same way you use it today in the react router:

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

Redirection logic

How you install this part depends on several factors:

  1. How do you determine if a user is logged in, is he on, is on a waiting list, etc.
  2. Which component you want to answer, where to redirect.

User Status Processing

Since you are using Apollo, you probably just want to use graphql to get this data in your HOC:

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

Then you can modify the Private component to get these details:

 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 several different places where you can handle this.

The easy way: routes are static

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

In this case, you can simply hardcode these routes into componentDidMount . Done.

The component is responsible

If you want the MyRoute component MyRoute determine the path, you can simply add some additional parameters to the privateRoute function and then pass them when exporting MyRoute .

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

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

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

Route is responsible

If you want to be able to transfer the path from routing, you want to receive details for them 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 an approach I like to use)

You can also let the component and route choose who is responsible. You just need to add privateRoute parameters for the paths, as we did to allow the component to solve. Then use these values ​​as defaultProps , as we did when the route was responsible.

This gives you the flexibility to make decisions. Just note that passing routes as a props will take precedence over passing from a component to the HOC.

Now all together

Here is a snippet combining all of the concepts above for the ultimate HOC study:

 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 use non-responsive static winch as stated in the official documentation .

+9
source

I think you need to adjust your logic a bit. Sort of:

 <Route path="/onboarding" render={renderProps=> <CheckAuthorization authorized={OnBoardingPage} renderProps={renderProps} /> }/> 
+1
source

I personally use to build my private routes as follows:

 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 returns true if the user is logged in (depending on how you process the user session), you can create each of your private routes as follows.

Then you can use it in Switch:

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

All routines from this PrivateRoute first need to check if the user is registered.

The last step is to nest routes according to their required status.

+1
source

You will need to use ApolloClient without the "reaction-graphql" HOC.
1. Get an instance of ApolloClient
2. Fire request
3. While Query returns a data load.

4. Check and authorize the route based on the data.
5. Return The corresponding component or redirection.

This can be done as follows:

 import Loadable from 'react-loadable' import client from '...your ApolloClient instance...' const queryPromise = client.query({ query: Storequery, variables: { name: context.params.sellername } }) const CheckedComponent = Loadable({ loading: LoadingComponent, loader: () => new Promise((resolve)=>{ queryPromise.then(response=>{ /* check response data and resolve appropriate component. if matching error return redirect. */ if(response.data.userStatus.isLoggedIn){ resolve(ComponentToBeRendered) }else{ resolve(<Redirect to={somePath}/>) } }) }), }) <Route path="/onboarding" component={CheckedComponent} /> 

Associated API link: https://www.apollographql.com/docs/react/reference/index.html

0
source

Source: https://habr.com/ru/post/1275245/


All Articles