How to use Apollo Client + React Router to implement private routes and redirection based on user status?
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:
- How you determine whether a user is logged in, onboarded, waitlisted, etc.
- 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.
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.