Photo by Lautaro Andreani on Unsplash

A Custom React Hook for handling client authentication and component-level error handling

Prachurjya Gogoi

--

In any React project, there is always a requirement for handling the user authentication state throughout the lifetime of the application and also while fetching or sending data to the backend.

So, here I’ll be showing how to create a custom react hook that worked wonders for me for handling both user auth state and also handling component-level errors.

I’ll be using Typescript with React for my explanation.

Let us think about our requirements:

  1. We need a state to indicate if the user is authenticated.
  2. A function to log in the user.
  3. A function to logout the user.
  4. An error handler that will receive an error object and will perform the required task necessary to handle that error.
  5. A function to renew the authentication state of the user if the token or session of the user expires in between the lifetime of the application.

Creating the hook

import { useState } from "react";

export const useAuth = () => {
const [authenticated, setAuthenticated] = useState(false);
const [renewInProgress, setRenewInProgress] = useState(false);

function loginUser() {}
function logoutUser() {}
function renewUserAuth() {}
function errorHandler() {}

return {
authenticated,
renewInProgress,
loginUser,
logoutUser,
errorHandler
};
};

This is the barebones structure of how the hook will look. From here on anybody can modify the hook how they prefer. I’ll be showing how I use it in my projects.

Now remember the states and the functions must be available to us throughout the application and a better way to do it is by making a context to pass down the values.

Creating a context to use the hook.

import { ReactNode, createContext, useContext } from "react";
import { useAuth } from "../hooks/useAuth";

interface IPorps {
children: ReactNode;
}

const authContext = createContext<ReturnType<typeof useAuth> | null>(null);

export function useAuthContext() {
return useContext(authContext);
}

const AuthProvider = ({ children }: IPorps) => {
const auth = useAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
};

export default AuthProvider;

Let me explain what is going on here. First of all, we created a context, initialized it with null, and named it authContext. Since we are using Typescript and to get the correct types for the return values of the useAuth hook, we passed a generic to createContext which is either null or it will be the return type of the useAuth hook.

const authContext = createContext<ReturnType<typeof useAuth> | null>(null);

Please refer to the typescript docs if you didn’t understand type generics and utility types.

Next, we create a function useAuthContext() to consume the context or we can say to use the context. I prefer to write this function so that I don’t have to import both the consumer and the context in my components, just the function, and it's done.

export function useAuthContext() {
return useContext(authContext);
}

Then we create the AuthProvider component where we will be passing down the values to the child components using the authContext that we created.

interface IPorps {
children: ReactNode;
}

const AuthProvider = ({ children }: IPorps) => {
const auth = useAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
};

export default AuthProvider;

Time to apply some good practices. What do I mean by that let’s see.

const loginUser = useCallback(()=>{},[])
const logoutUser = useCallback(()=>{},[])
const errorHandler = useCallback(()=>{},[])

Wrapped the above functions with useCallback with an empty dependency array as these functions are not dependent on the states of the useAuth hook and by doing so the referential equality of the functions will also be maintained as we pass them down to the child components.

Now, let’s implement the errorHandler() function.

interface IMyError {
status: number;
}

const errorHandler = useCallback(
<T extends IMyError>(cb: (...args: any) => void) => {
return function (error: AnyErrorClass<T>) {
if (error.status === 401) {
renewUserAuth(); // this re-authenticates the user
} else {
cb(error); // this is used to handle other api errors in the component
}
};
},
[]
);

Okay, so the function may look intimidating at first glance but let me explain what is going on. First, we take a generic T that extends our own error interface so that we can have custom properties on the error object.

Next, we accept a callback in the errorHandler() that returns a function that accepts an Error object. We then check if the error status (which in this case is my custom property) equals 401 (general error code for not authenticated). In this case, we re-authenticate the user and if it is any other error then we invoke the passed callback with the passed error.

Usage example.

// useAuth.ts

import { useCallback, useState } from "react";
import { AxiosError } from "axios";

export interface IMyError {
status: number;
}

export const useAuth = () => {
const [authenticated, setAuthenticated] = useState(false);
const [renewInProgress, setRenewInProgress] = useState(false);

function renewUserAuth() {
setRenewInProgress(true);
{
/** logic to renew the user */
}
setRenewInProgress(false);
}

const loginUser = useCallback(() => {
{
/** login user logic */
}
setAuthenticated(true);
}, []);

const logoutUser = useCallback(() => {
{
/** logout user logic */
}
setAuthenticated(false);
}, []);

const errorHandler = useCallback(
<T extends IMyError>(cb: (...args: any) => void) => {
return function (error: AxiosError<T>) {
if (error.status === 401) {
renewUserAuth();
} else {
cb(error.message);
}
};
},
[]
);

return {
authenticated,
renewInProgress,
loginUser,
logoutUser,
errorHandler
};
};
// App.tsx

import { useEffect, useState } from "react";
import { useAuthContext } from "./providers/AuthProvider";
import axios, { AxiosError } from "axios";
import { IMyError } from "./hooks/useAuth";
import "./styles.css";

export default function App() {
const [data, setData] = useState("");
const auth = useAuthContext();

async function handleGetData() {
try {
const response = await axios.get<string>("https://example.com/myData");
setData(response.data);
} catch (error) {
const err = error as AxiosError<IMyError>;
auth?.errorHandler(handleError)(err);
}
}

function handleError(msg: string) {
console.error("error", msg);
}

useEffect(() => {
handleGetData();
}, []);

return <div>{data}</div>;
}

So, as you can see this is a very convenient way of handling the errors, and the best part, this hook pattern can be modified to your use case very easily.

That’s all for this post hope you learned something new…

--

--