Alex Sidorenko

Protected routes in Next.js compared to React Router

June 21, 2021

Coming from React Router, Next.js route authentication patterns can seem confusing. Something you can accomplish in one line with React Router is a bit more complicated in Next.js.

Protected route with React Router:

{isAuthorized && <Route path='/about' ... />}

What is the Next.js equivalent of this code?

Disclaimer

In this article, we will only focus on static rendering authentication. It is the closest concept to the usual React implementation with React Router. If you want to do server-side authentication, check out the corresponding section of the docs. Now, let’s move on.

Official documentation

Next.js documentation has this example of the protected route implementation:

// pages/profile.js

import useUser from '../lib/useUser'
import Layout from '../components/Layout'

const Profile = () => {
  // Fetch the user client-side
  const { user } = useUser({ redirectTo: '/login' })

  // Server-render loading state
  if (!user || user.isLoggedIn === false) {
    return <Layout>Loading...</Layout>
  }

  // Once the user request finishes, show the user
  return (
    <Layout>
      <h1>Your Profile</h1>
      <pre>{JSON.stringify(user, null, 2)}</pre>
    </Layout>
  )
}

export default Profile

But do you repeat this code for every single page? What if you want to add some additional logic, like user types? Should you repeat this on every page too? Well, not really.

Move user logic to _app.js

To get more control over the routes, we can modify default _app.js.

// pages/_app.js

import { UserContext } from "../components/user";

function MyApp({ Component, pageProps }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    /**
     * Here goes the logic of retrieving a user
     * from the backend and redirecting
     * an unauthorized user
     * to the login page
    */
    setUser(result)
  }, []);

  if (pageProps.protected && !user) {
    return (
      <Layout>Loading...</Layout>
    )
  }

  return (
    <UserContext.Provider value={user}>
      <Component {...pageProps} />
    </UserContext.Provider>
  );
}
// components/user.js

import { createContext, useContext } from "react";

export const UserContext = createContext(null);

export const useUser = () => {
  return useContext(UserContext);
};

Now we can use getStaticProps to make any page protected.

// pages/profile.js

const Profile = () => {
  
  return (
    <Layout>
      <h1>Your Profile</h1>
    </Layout>
  )
}

export async function getStaticProps(context) {
  return {
    props: {
      protected: true
    }
  };
}

export default Profile

User types

Let’s add user types logic to our _app.js.

// pages/_app.js

function MyApp({ Component, pageProps }) {
  const [user, setUser] = useState(null);

  ...

  if (pageProps.protected && !user) {
    return (
      <Layout>Loading...</Layout>
    )
  }

  if (
    pageProps.protected &&
    user &&
    pageProps.userTypes &&
    pageProps.userTypes.indexOf(user.type) === -1
  ) {
    return <Layout>Sorry, you don't have access</Layout>;
  }

  return (
    <UserContext.Provider value={user}>
      <Component {...pageProps} />
    </UserContext.Provider>
  );
}

Now we can specify allowed user types for a protected route.

// pages/admin.js

const Admin = () => {

  const { user } = useUser()
  
  return (
    <Layout>
      <h1>Hi {user.name}</h1>
      <p>Welcome to admin dashboard</p>
    </Layout>
  )
}

export async function getStaticProps(context) {
  return {
    props: {
      protected: true,
      userTypes: ['admin']
    }
  };
}

export default Admin

That’s it. Here is a demo. I hope this helps to create protected routes in Next.js for those coming from React Router.

Want to get better at modern React?

Subscribe to get one short article delivered to your inbox every week

One article a week. No spam.
Unsubscribe any time