tutorial // Apr 07, 2021
How to Handle Authenticated Routes with Next.js
How to create a HOC (higher-order component) that can conditionally redirect a user based on their logged in or logged out status.
In Next.js, by default, all of your routes are treated the same.
While your specific app may include pages or routes that are intended only for logged-in users, out of the box, Next.js does not provide a way to isolate these pages based on a user's authentication status.
This is expected as Next.js is designed to handle a simple, well-defined set of tasks. While it can be used as a front-end for an application—like in CheatCode's Next.js boilerplate—traditionally it's used to generate static marketing sites or sites backed by a headless CMS.
Fortunately, solving this problem isn't too complex. To solve it, we're going to implement two components:
authenticatedRoute
which will be a function that returns a React component wrapped with a conditional check for the user's authentication status and a redirect if a user is unavailable.publicRoute
which will be a function that returns a React component wrapped with a conditional check for the user's authentication status and a redirect if a user is present.
Implementing an Authenticated Route Component
First, let's build out the skeleton for our HOC and discuss how it's going to work:
/components/AuthenticatedRoute/index.js
import React from "react";
const authenticatedRoute = (Component = null, options = {}) => {
// We'll handle wrapping the component here.
};
export default authenticatedRoute;
Here, we export a plain JavaScript function that takes two arguments: a React Component
as the first argument and an object of options
as the second. The Component
represents the component of the protected page that we want to conditionally render.
When we go to put this to use, we'll do something like this:
/pages/<somePage>/index.js
import authenticatedRoute from '../../components/AuthenticatedRoute';
const MyComponent = () => {
[...]
};
export default authenticatedRoute(MyComponent, { pathAfterFailure: '/login' })
Moving forward, let's populate our HOC with the core wrapper component:
/components/AuthenticatedRoute/index.js
import React from "react";
const authenticatedRoute = (Component = null, options = {}) => {
class AuthenticatedRoute extends React.Component {
state = {
loading: true,
};
render() {
const { loading } = this.state;
if (loading) {
return <div />;
}
return <Component {...this.props} />;
}
}
return AuthenticatedRoute;
};
export default authenticatedRoute;
Here, we've populated our authenticatedRoute
function's body with a class-based React component. The idea here is that we want to utilize the state and—next—the componentDidMount
function for the class so that we can decide if we want to render the passed Component
, or, redirect the user away from it.
/components/AuthenticatedRoute/index.js
import React from "react";
import Router from "next/router";
const authenticatedRoute = (Component = null, options = {}) => {
class AuthenticatedRoute extends React.Component {
state = {
loading: true,
};
componentDidMount() {
if (this.props.isLoggedIn) {
this.setState({ loading: false });
} else {
Router.push(options.pathAfterFailure || "/login");
}
}
render() {
const { loading } = this.state;
if (loading) {
return <div />;
}
return <Component {...this.props} />;
}
}
return AuthenticatedRoute;
};
export default authenticatedRoute;
Now, with our componentDidMount
added, we can see our core behavior implemented. Inside, all we want to know is "is there a logged in user or not?" If there is a logged in user, we want to say "go ahead and render the passed Component
." We can see this taking place down in the render()
method of the AuthenticatedRoute
component.
Here, we're saying, as long as loading
is true
, just return an empty <div />
(or, show nothing to the user). If we're not loading, just run the return
statement at the bottom of the render()
.
What this achieves is saying "until we know that we have a logged in user, show nothing, and if we do have a logged in user, show them the page they're trying to access."
Back in componentDidMount()
in the else
statement, we're saying "okay, it doesn't look like the user is logged in so let's redirect them." To do the redirect in this example, we're using the built-in Next.js router to do the redirect for us, but you could use any JavaScript or React router you'd like (e.g., if we were using React Router we'd do this.props.history.push(options.pathAfterFailure || '/login')
.
Make sense? So, if we have a user, show them the component. If we don't have a user, redirect them to another route.
Determining logged-in status
Now, technically speaking, this is all we have to do. But you may be wondering "how do we know if the user is logged in?" This is where your own app comes in. In this example, we're using the CheatCode Next.js Boilerplate which relies on an authenticated user (if available) being present in a global Redux store.
To make this all a little more concrete, let's take a look at that setup now:
/components/AuthenticatedRoute/index.js
import React from "react";
import Router from "next/router";
import { connect } from "react-redux";
const authenticatedRoute = (Component = null, options = {}) => {
class AuthenticatedRoute extends React.Component {
state = {
loading: true,
};
componentDidMount() {
if (this.props.isLoggedIn) {
this.setState({ loading: false });
} else {
Router.push(options.pathAfterFailure || "/login");
}
}
render() {
const { loading } = this.state;
if (loading) {
return <div />;
}
return <Component {...this.props} />;
}
}
return connect((state) => ({
isLoggedIn: state?.authenticated && !!state?.user,
}))(AuthenticatedRoute);
};
export default authenticatedRoute;
The big change we've made here is to import the connect()
method from the react-redux
package (already installed in the boilerplate) and then call that function, passing it a mapStateToProps
function and then wrapping it around our component. To be clear, this part:
/components/AuthenticatedRoute/index.js
return connect((state) => ({
isLoggedIn: state?.authenticated && !!state?.user,
}))(AuthenticatedRoute);
Here, the function that we pass as the first argument to connect()
is the mapStateToProps
function (as it's named in the react-redux
documentation). This function takes in the current global state for the application provided by the <ReduxProvider />
in /pages/_app.js
in the CheatCode Next.js boilerplate.
Using that state, like the name implies, it maps that state to a React component prop that will be handed down to our <AuthenticatedRoute />
component defined just above it.
If we look close, here, we're setting a prop called isLoggedIn
, checking to see if the authenticated
value on our state is true
and whether or not we have a user
object on state. If we do? The user is logged in! If not, isLoggedIn
is false.
If you look back in the componentDidMount()
function, this is where we're putting the new isLoggedIn
prop to use.
Using other authentication sources
If you're not using the CheatCode Next.js Boilerplate, how you get to your user's authenticated state depends on your app. A quick and dirty example of using another API would look something like this:
import React from "react";
import Router from "next/router";
import { connect } from "react-redux";
import { myAuthenticationAPI } from 'my-authentication-api';
const authenticatedRoute = (Component = null, options = {}) => {
class AuthenticatedRoute extends React.Component {
state = {
loading: true,
};
async componentDidMount() {
const isLoggedIn = await myAuthenticationAPI.isLoggedIn();
if (isLoggedIn) {
this.setState({ loading: false });
} else {
Router.push(options.pathAfterFailure || "/login");
}
}
render() {
const { loading } = this.state;
if (loading) {
return <div />;
}
return <Component {...this.props} />;
}
}
return AuthenticatedRoute;
};
export default authenticatedRoute;
In this example, nearly everything is identical, but instead of anticipating an authentication value coming from a Redux store, we just call to our authentication API (e.g., Firebase) directly, relying on the return value to that call as our isLoggedIn
status.
Implement a Public Route Component
Now, some good news: our publicRoute
component is identical to what we looked at above with one tiny little change:
/components/PublicRoute/index.js
import React from "react";
import Router from "next/router";
import { connect } from "react-redux";
const publicRoute = (Component = null, options = {}) => {
class PublicRoute extends React.Component {
state = {
loading: true,
};
componentDidMount() {
if (!this.props.isLoggedIn) {
this.setState({ loading: false });
} else {
Router.push(options.pathAfterFailure || "/documents");
}
}
render() {
const { loading } = this.state;
if (loading) {
return <div />;
}
return <Component {...this.props} />;
}
}
return connect((state) => ({
isLoggedIn: state?.authenticated && !!state?.user,
}))(PublicRoute);
};
export default publicRoute;
Can you spot it? Up in the componentDidMount
we've added a !
to say "if the user is not logged in, go ahead and render the component. If they are logged in, redirect them."
Literally the inverse logic to our authenticatedRoute
. The point here is that we want to use the publicRoute()
component on routes like /login
or /signup
to redirect already authenticated users away from those pages. This ensures we don't get database messes later like duplicate users or multiple user sessions.
Wrapping up
In this tutorial, we learned a simple pattern for creating a HOC (higer-order component) for redirecting users in our app based on their logged in (authentication) status. We learned how to implement the base component that "wraps" around the componetn we're trying to protect and how to implement the core logic to handle the render and redirect process.
We also looked at examples of using actual authentication data to add some context and clarify how this pattern can work in any authentication setup within Next.js.