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:
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.
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.
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.
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
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}
/>
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} />
La manière dont vous configurez cette partie dépend de deux facteurs:
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} />
)
}
}
Vous pouvez gérer cela à différents endroits.
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é.
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)
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" />} />
(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.
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 .
Je pense que vous devez déplacer votre logique un peu. Quelque chose comme:
<Route path="/onboarding" render={renderProps=>
<CheckAuthorization authorized={OnBoardingPage} renderProps={renderProps} />
}/>
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.
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