web-dev-qa-db-fra.com

Comment utiliser Apollo Client + React Router pour implémenter des routes privées et une redirection en fonction du statut de l'utilisateur?

J'utilise React Router 4 pour le routage et Apollo Client pour la récupération et la mise en cache des données. Je dois implémenter une solution PrivateRoute et de redirection basée sur les critères suivants:

  1. Les pages qu'un utilisateur est autorisé à voir sont basées sur son statut, qui peut être récupéré sur le serveur ou lu à partir du cache. Le statut d'utilisateur est essentiellement un ensemble d'indicateurs que nous utilisons pour comprendre où se trouve l'utilisateur dans notre entonnoir. Exemple de drapeaux: isLoggedIn, isOnboarded, isWaitlisted etc.

  2. Aucune page ne devrait même commencer à afficher si le statut de l'utilisateur ne lui permet pas de figurer sur cette page. Par exemple, si vous n'êtes pas isWaitlisted, vous n'êtes pas censé voir la page de liste d'attente. Lorsque des utilisateurs se retrouvent accidentellement sur ces pages, ils doivent être redirigés vers une page adaptée à leur statut.

  3. La redirection doit également être dynamique. Par exemple, supposons que vous essayiez de voir votre profil d'utilisateur avant que vous soyez isLoggedIn. Ensuite, nous devons vous rediriger vers la page de connexion. Cependant, si vous êtes isLoggedIn mais pas isOnboarded, nous ne voulons toujours pas que vous voyiez votre profil. Nous voulons donc vous rediriger vers la page d'intégration.

  4. Tout cela doit se passer au niveau de la route. Les pages elles-mêmes ne doivent pas être au courant de ces permissions et redirections. 

En conclusion, nous avons besoin d’une bibliothèque qui, étant donné les données de statut de l’utilisateur, puisse 

  • calculer si un utilisateur peut être sur une certaine page
  • calculer où ils doivent être redirigés dynamiquement
  • faites-les avant de rendre n'importe quelle page
  • faites-les au niveau de la route

Je travaille déjà sur une bibliothèque à usage général, mais elle présente actuellement des lacunes. Je cherche des avis sur la manière d'aborder ce problème et sur les modèles établis pour atteindre cet objectif.

Voici mon approche actuelle. Cela ne fonctionne pas car les données dont la getRedirectPath a besoin sont dans le OnboardingPage component

De plus, je ne peux pas envelopper PrivateRoute avec le HOC qui pourrait injecter les accessoires nécessaires pour calculer le chemin de redirection, car cela ne me permettrait pas de l'utiliser comme enfant du composant Switch React Router, car il cesse d'être une 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
AnApprentice

Approche générale

Je créerais un HOC pour gérer cette logique pour toutes vos 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'
  })`

  // ...and returns a new component wrapping the parameter component
  return hoistNonReactStatics(Private, WrappedComponent)
}

export default privateRoute

Ensuite, il vous suffit de modifier la manière dont vous exportez vos itinéraires:

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

et vous pouvez utiliser cette route de la même manière que vous le faites aujourd'hui dans react-router:

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

Logique de redirection

La manière dont vous configurez cette partie dépend de deux facteurs:

  1. Comment déterminer si un utilisateur est connecté, intégré, inscrit sur une liste d'attente, etc.
  2. Quel composant vous voulez être responsable de where pour rediriger.

Gestion du statut d'utilisateur

Puisque vous utilisez Apollo, vous voudrez probablement simplement utiliser graphql pour récupérer ces données dans votre HOC:

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

Ensuite, vous pouvez modifier le composant Private pour récupérer ces accessoires:

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} />
    )
  }
}

Où rediriger

Vous pouvez gérer cela à différents endroits.

Moyen facile: les routes sont statiques

Si un utilisateur n'est pas connecté, vous voulez toujours router vers /login?return=${currentRoute}.

Dans ce cas, vous pouvez simplement coder ces routes en dur dans votre componentDidMount. Terminé.

Le composant est responsable

Si vous voulez que votre composant MyRoute détermine le chemin, vous pouvez simplement ajouter des paramètres supplémentaires à votre fonction privateRoute, puis les transmettre lorsque vous exportez MyRoute.

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

Ensuite, si vous souhaitez remplacer le chemin par défaut, modifiez votre exportation comme suit:

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

La route est responsable

Si vous voulez pouvoir passer le chemin à partir du routage, vous voudrez recevoir des accessoires pour ceux-ci dans 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'
}

Ensuite, votre itinéraire peut être réécrit pour:

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

Combinez les options 2 et 3

(C'est l'approche que j'aime utiliser)

Vous pouvez également laisser le composant et la route choisir le responsable. Vous devez simplement ajouter les paramètres privateRoute pour les chemins d'accès, comme nous l'avons fait pour laisser le composant décider. Utilisez ensuite ces valeurs en tant que defaultProps comme nous le faisions lorsque la route était responsable.

Cela vous donne la flexibilité de décider au fur et à mesure. Il suffit de noter que le fait de passer des chemins comme accessoires aura la priorité sur le passage du composant dans le HOC.

Tous ensemble maintenant

Voici un extrait combinant tous les concepts ci-dessus pour une version finale du 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'
  })`

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

export default privateRoute


J'utilise hoist-non-react-statics comme suggéré dans la documentation officielle .

9
Luke M Willis

Je pense que vous devez déplacer votre logique un peu. Quelque chose comme: 

<Route path="/onboarding" render={renderProps=>
   <CheckAuthorization authorized={OnBoardingPage} renderProps={renderProps} />
}/>
1
Dennie de Lange

J'utilise personnellement pour construire mes routes privées comme ceci:

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} />
      ))
    }
  />
);

Dans ce cas, loggedIn() est une fonction simple qui renvoie true si l'utilisateur est connecté (dépend de la façon dont vous gérez la session utilisateur), vous pouvez créer chacun de vos itinéraires privés de la manière suivante.

Ensuite, vous pouvez l’utiliser dans un commutateur:

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

Tous les sous-itinéraires de cette PrivateRoute devront d'abord vérifier si l'utilisateur est connecté.

La dernière étape consiste à imbriquer vos itinéraires en fonction de leur statut requis.

1
Dyo

Vous devrez utiliser ApolloClient sans HOC 'react-graphql'.
1. Obtenir une instance de ApolloClient
2. Requête d'incendie
3. Pendant que Query retourne le chargement du rendu des données.
4. Vérifiez et autorisez un itinéraire en fonction des données.
5. Renvoyez le composant approprié ou redirigez-le. 

Cela peut être fait de la manière suivante:

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} />

Référence API associée: https://www.apollographql.com/docs/react/reference/index.html

0
Chromonav