Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?

Next.js Authentication using Higher-Order Components



Introduction

Managing authentication in Subsequent.js is kind of tough, with issues reminiscent of content material flashing. On this weblog, I will not deal with the issues and clarify the way to remedy it intimately, as a result of I’ve written a weblog about that in Next.js Redirect Without Flashing Content.

On this weblog, I am going to cowl the way to deal with them cleanly utilizing Greater Order Parts.



The Traditional Approach & The Drawback

Often for the authentication in Subsequent.js, we outline routes that should be blocked like so:

const protectedRoutes = ['/block-component', '/profile'];
Enter fullscreen mode

Exit fullscreen mode

Then we’ve got a element that checks the route like this:

export default operate PrivateRoute({ protectedRoutes, kids }) {
  const router = useRouter();
  const { isAuthenticated, isLoading } = useAuth();

  const pathIsProtected = protectedRoutes.indexOf(router.pathname) !== -1;

  useEffect(() => {
    if (!isLoading && !isAuthenticated && pathIsProtected) {
      // Redirect route, you may level this to /login
      router.push('/');
    }
  }, [isLoading, isAuthenticated, pathIsProtected]);

  if ((isLoading || !isAuthenticated) && pathIsProtected) {
    return <FullPageLoader />;
  }

  return kids;
}
Enter fullscreen mode

Exit fullscreen mode

This works, however there are a number of issues:

  1. It is not colocated, the location of authentication is just not positioned within the web page itself, as a substitute in one other element reminiscent of PrivateRoute
  2. Error Inclined, whenever you’re doing route adjustments, for instance: when you’re shifting the pages/blocked-component.tsx file to pages/blocked/element.tsx, you’ll have to change the protectedRoutes variable into the brand new route.

That is fairly harmful as a result of with the protectedRoutes variable, there are not any kind checking as a result of there is no such thing as a method for TypeScript to know if that is the suitable path. (maybe soon)



Greater-Order Element

My good friend and I constructed a higher-order element that we are able to put contained in the web page like so:

export default withAuth(ProtectedPage);
operate ProtectedPage() {
  /* react element right here */
}
Enter fullscreen mode

Exit fullscreen mode

With this implementation, it is now colocated inside the web page and it will not be an issue when you change the file identify.



Including A number of Forms of Pages

In my expertise of constructing easy authenticated apps, there are 3 kind of authenticated pages that we have to help

For the demo, you may attempt it your self on the demo page



1. Easy Protected Pages

It is for pages that want safety, reminiscent of dashboard, edit profile web page, and many others.

Conduct

  • Unauthenticated customers can be redirected to LOGIN_ROUTE (default: /login), with none content material flashing
  • Authenticated customers will see this web page on this following state of affairs:

    • Direct go to utilizing hyperlink → person will see a loading web page whereas the withAuth element checks the token, then this web page can be proven
    • Go to from different pages (router.push) → person will see this web page instantly



2. Authentication Pages (Login)

It is for pages reminiscent of Login and Register or another web page that fits with the conduct.

Conduct:

  • Unauthenticated customers can entry this web page with none loading indicator
  • Authenticated customers can be redirected to HOME_ROUTE (default: /).

    • We’re assuming that authenticated customers will not must see login anymore. As an alternative, they need to be redirected to the HOME_ROUTE.
    • It is also finest to cover all hyperlinks again to the login web page when the customers is already authenticated.



3. Non-compulsory Web page

This can be a extra particular use case, however typically there are pages that you do not should be authenticated to go to, however you continue to want to point out the customers particulars if they’re authenticated.

Conduct:

  • This web page is accessible to all customers
  • You will get the person from useAuthStore.useUser()



Web page Focus Synchronization


We additionally added a web page focus listener. If you open a number of tabs, the authentication can be synced throughout tabs.

React.useEffect(() => {
  // run checkAuth each web page go to
  checkAuth();

  // run checkAuth each focus adjustments
  window.addEventListener('focus', checkAuth);
  return () => {
    window.removeEventListener('focus', checkAuth);
  };
}, [checkAuth]);
Enter fullscreen mode

Exit fullscreen mode



Supply Codes

We use Zustand to retailer authentication information globally



Zustand Retailer

import { createSelectorHooks } from 'auto-zustand-selectors-hook';
import produce from 'immer';
import create from 'zustand';

import { Person } from '@/varieties/auth';

kind AuthStoreType =  null;
  isAuthenticated: boolean;
  isLoading: boolean;
  login: (person: Person) => void;
  logout: () => void;
  stopLoading: () => void;
;

const useAuthStoreBase = create<AuthStoreType>((set) => ({
  person: null,
  isAuthenticated: false,
  isLoading: true,
  login: (person) => {
    localStorage.setItem('token', person.token);
    set(
      produce<AuthStoreType>((state) => {
        state.isAuthenticated = true;
        state.person = person;
      })
    );
  },
  logout: () => {
    localStorage.removeItem('token');
    set(
      produce<AuthStoreType>((state) => {
        state.isAuthenticated = false;
        state.person = null;
      })
    );
  },
  stopLoading: () => {
    set(
      produce<AuthStoreType>((state) => {
        state.isLoading = false;
      })
    );
  },
}));

const useAuthStore = createSelectorHooks(useAuthStoreBase);

export default useAuthStore;
Enter fullscreen mode

Exit fullscreen mode



withAuth HOC Element

import { useRouter } from 'subsequent/router';
import * as React from 'react';
import { ImSpinner8 } from 'react-icons/im';

import apiMock from '@/lib/axios-mock';
import { getFromLocalStorage } from '@/lib/helper';

import useAuthStore from '@/retailer/useAuthStore';

import { ApiReturn } from '@/varieties/api';
import { Person } from '@/varieties/auth';

export interface WithAuthProps {
  person: Person;
}

const HOME_ROUTE = '/';
const LOGIN_ROUTE = '/login';

const ROUTE_ROLES = [
  /**
   * For authentication pages
   * @example /login /register
   */
  'auth',
  /**
   * Optional authentication
   * It doesn't push to login page if user is not authenticated
   */
  'optional',
  /**
   * For all authenticated user
   * will push to login if user is not authenticated
   */
  'all',
] as const;
kind RouteRole = (typeof ROUTE_ROLES)[number];

/**
 * Add role-based entry management to a element
 *
 * @see https://react-typescript-cheatsheet.netlify.app/docs/hoc/full_example/
 * @see https://github.com/mxthevs/nextjs-auth/blob/essential/src/elements/withAuth.tsx
 */
export default operate withAuth<T extends WithAuthProps = WithAuthProps>(
  Element: React.ComponentType<T>,
  routeRole: RouteRole
) {
  const ComponentWithAuth = (props: Omit<T, keyof WithAuthProps>) => {
    const router = useRouter();
    const { question } = router;

    //#area  //*=========== STORE ===========
    const isAuthenticated = useAuthStore.useIsAuthenticated();
    const isLoading = useAuthStore.useIsLoading();
    const login = useAuthStore.useLogin();
    const logout = useAuthStore.useLogout();
    const stopLoading = useAuthStore.useStopLoading();
    const person = useAuthStore.useUser();
    //#endregion  //*======== STORE ===========

    const checkAuth = React.useCallback(() => {
      const token = getFromLocalStorage('token');
      if (!token) {
        isAuthenticated && logout();
        stopLoading();
        return;
      }
      const loadUser = async () => {
        attempt {
          const res = await apiMock.get<ApiReturn<Person>>('/me');

          login({
            ...res.information.information,
            token: token + '',
          });
        } catch (err) {
          localStorage.removeItem('token');
        } lastly {
          stopLoading();
        }
      };

      if (!isAuthenticated) {
        loadUser();
      }
    }, [isAuthenticated, login, logout, stopLoading]);

    React.useEffect(() => {
      // run checkAuth each web page go to
      checkAuth();

      // run checkAuth each focus adjustments
      window.addEventListener('focus', checkAuth);
      return () => {
        window.removeEventListener('focus', checkAuth);
      };
    }, [checkAuth]);

    React.useEffect(() => {
      if (!isLoading) {
        if (isAuthenticated) {
          // Forestall authenticated person from accessing auth or different position pages
          if (routeRole === 'auth') {
            if (question?.redirect) {
              router.substitute(question.redirect as string);
            } else {
              router.substitute(HOME_ROUTE);
            }
          }
        } else {
          // Forestall unauthenticated person from accessing protected pages
          if (routeRole !== 'auth' && routeRole !== 'elective') {
            router.substitute(
              `${LOGIN_ROUTE}?redirect=${router.asPath}`,
              `${LOGIN_ROUTE}`
            );
          }
        }
      }
    }, [isAuthenticated, isLoading, query, router, user]);

    if (
      // If unauthenticated person wish to entry protected pages
      (isLoading || !isAuthenticated) &&
      // auth pages and elective pages are allowed to entry with out login
      routeRole !== 'auth' &&
      routeRole !== 'elective'
    ) {
      return (
        <div className='flex min-h-screen flex-col items-center justify-center text-gray-800'>
          <ImSpinner8 className='mb-4 animate-spin text-4xl' />
          <p>Loading...</p>
        </div>
      );
    }

    return <Element {...(props as T)} person={person} />;
  };

  return ComponentWithAuth;
}
Enter fullscreen mode

Exit fullscreen mode

For extra code and implementation examples try the code on GitHub



Attribution

  • Rizqi Tsani, co-creator of this code.
  • Next Auth, for the inspiration and the concept of utilizing HOC to deal with authentication.



Conclusion

This can be an amazing addition to your code, making it cleaner and extra environment friendly. It is best to colocate your code as a lot as potential, and this can be a step to do this.


Initially posted on my personal site, discover extra blog posts and code snippets library I put up for simple entry on my website 🚀

Like this submit? Subscribe to my newsletter to get notified each time a brand new submit is out!

Add a Comment

Your email address will not be published. Required fields are marked *

Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?