Creating a Simple Authentication Context in Your React Project: A Step-by-Step Guide

0 min Reading Time

https://cdn.sanity.io/images/957rf3u0/production/85f3e22da368307cb3ae4d17cd3388218c06b6de-1457x820.jpg?fit=max&auto=format

Building a secure and seamless authentication system is essential to protect user data and provide personalized experiences. In this tutorial, we will delve into the process of creating an authentication context in your project using React.

By implementing an auth context, you can centralize and manage authentication-related states throughout your application. This powerful approach simplifies the sharing of user authentication data across components, eliminating the need for prop drilling or complex state management solutions.

Throughout this guide, we will explore the step-by-step process of setting up an auth context, integrating authentication functionality, and consuming the context in various components. By the end, you will have a solid understanding of how to create a flexible and scalable authentication system in your React projects.

Whether you're working on a personal project, a small application, or a large-scale enterprise application, mastering the art of creating an auth context will undoubtedly enhance your front-end development skills. So, let's dive in and unlock the secrets of building a robust authentication system using React's auth context!

In order to implement the auth context into our React project, first we need to build the AuthContext.tsx file. Let’s create a User interface that will return from the backend on our calls.

interface User {
  status: string;
  _id: string;
  email: string;
  nameSurname: string;
  confirmationCode: string;
  __v: number;
  tempSecret: string;
  tempToken: string;
  secret: string;
  token: string;
  lastAccessed: string;
}

In order for Typescript to not throw any error on our code, we need to create an initial user that will look like this.

const initialUser: User = {
  status: '',
  followers: [],
  _id: '',
  email: '',
  nameSurname: '',
  confirmationCode: '',
  __v: 7,
  tempSecret: '',
  tempToken: '',
  etsyId: '',
  secret: '',
  token: '',
  lastAccessed: '',
};

This is not mandatory and there are other ways of doing it but I prefer to do it this way. Now that we know what our User object looks like, we can start working on our AuthContext logic. Apart from the user object, we’ll return some functions in the AuthContext such as loginUser, logoutUser, registerUser, changePassword, and forgetPassword. These will be empty functions with userData as props. No need to worry about these empty functions because in the next step, we’ll introduce the actual functions.

export const AuthContext = createContext({
  user: initialUser,
  loginUser: (userData: User) => {},
  logoutUser: () => {},
  registerUser: (userData: User) => {},
  changePassword: (userData: User) => {},
  forgetPassword: (userData: User) => {},
});

After we defined our AuthContext, now we need to create an AuthProvider which will wrap up our application. AuthProvider will be the function where all the backend calls are made, and the state of the user is preserved. The AuthProvider will be in the shape of;

export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const navigate = useNavigate();
	const storedData = localStorage.getItem('authData');

  const [user, setUser] = useState(storedData ? JSON.parse(storedData) : undefined);

  const loginUser = (userData: User) => {
    axios
      .post(`${LOGIN_USER_ENDPOINT}`, userData)
      .then((res) => {
        setUser(decoded);
        localStorage.setItem('authData', JSON.stringify(decoded));
        navigate('/dashboard');
      })
      .catch((err) => {
				// Handle Error
      });
  };

  const logoutUser = () => {
    setUser(null);
    localStorage.removeItem('authData');
    navigate('/');
  };

  const registerUser = (userData: User) => {
    axios
      .post(`${CREATE_USER_ENDPOINT}`, userData)
      .then((res) => {
        setUser(res.data);
        localStorage.setItem('authData', JSON.stringify(data));
        navigate('/register-success');
      })
      .catch((err) => {
				// Handle Error
			});
  };

  const changePassword = (payload: User) => {
   axios
    .post(`${CHANGE_PASSWORD_ENDPOINT}`, payload)
	    .then(() => 
        navigate('/dashboard');
      })
      .catch((err) => {
	      // Handle Error
      });
  };

  const forgetPassword = async (payload: User) => {
    axios
      .post(`${FORGET_PASSWORD_ENDPOINT}`, payload)
      .then(() => {
			 	 // Handle Success
      })
      .catch((err) => {
         // Handle Error
      });
  };

  const authContextValue = {
    user,
    loginUser,
    logoutUser,
    registerUser,
    changePassword,
    forgetPassword,
  };

  return <AuthContext.Provider value={authContextValue}>{children}</AuthContext.Provider>;
};

Let’s break down this code to understand what is happening in each step;

  1. The AuthProvider the component is exported as a named export using export const. It takes a single prop children of type ReactNode, which represents the child components that will have access to the authentication context.
  2. The navigate function from the useNavigate hook is assigned to the navigate variable. This hook provides the ability to navigate to different routes programmatically.
  3. The storedData the variable is assigned the value retrieved from the localStorage the key 'authData'. It stores the serialized user authentication data if it exists.
  4. The user state variable is declared using the useState hook. It is initialized with the parsed value of storedDataif it exists, or undefined otherwise. This state variable represents the currently authenticated user.
  5. The loginUser function takes a userData object as a parameter, which represents the user credentials. It makes an asynchronous HTTP POST request using axios to a login endpoint (LOGIN_USER_ENDPOINT) with the provided user data. If the request is successful (then block), it sets the user state with the decoded response data stores the serialized user data in, and navigates the user to the '/dashboard' route. If the request fails (catch block), you can add an error handling code to handle the error appropriately.
  6. The logoutUser the function is responsible for logging out the user. It sets the user state to null, removes the serialized user data from, and navigates the user to the '/' route.
  7. The registerUser function is similar to loginUser, but it sends a POST request to a register endpoint (CREATE_USER_ENDPOINT) with the provided user data. On success, it sets the user state with the response data, stores the serialized user data in localStorage, and navigates the user to the '/register-success' route. You can add error handling code in case the request fails.
  8. The changePassword function is responsible for changing the user's password. It sends a POST request to a change password endpoint (CHANGE_PASSWORD_ENDPOINT) with the provided new password. On success, it navigates the user to the '/dashboard' route. You can handle errors appropriately in the catch block.
  9. The forgetPassword function handles the process of requesting a password reset. It sends a POST request to a forget password endpoint (FORGET_PASSWORD_ENDPOINT) with the provided payload and it’ll send you a link to change your password. You can handle success and error scenarios accordingly.
  10. An authContextValue object is created, which contains the user state, along with the defined authentication functions (loginUser, logoutUser, registerUser, changePassword, and forgetPassword).
  11. The AuthProvider component returns the AuthContext.Provider component, which wraps the children prop. It provides the authContextValue as the value of the authentication context, allowing the child components to access the user state and authentication functions.

Now the last part for this context to work is to wrap the whole application with it. in your index.ts file, we can do that like this.

import App from './App';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from 'context/auth/AuthContext';

const container = document.getElementById('root');
const root = createRoot(container as HTMLDivElement);

root.render(
  <StrictMode>
    <BrowserRouter>
      <AuthProvider>
        <App />
      </AuthProvider>
    </BrowserRouter>
  </StrictMode>
);

Now, we can consume our auth context anywhere in our project. To do that, I would like to create a custom hook for it. Let’s call it useAuth, this way, our code will look much more cleaner and compact.

import { useContext } from 'react';
import { AuthContext } from 'context/auth/AuthContext';

export const useAuth = () => {
  const authContext = useContext(AuthContext);

  if (!authContext) {
    throw new Error('useAuth must be used within an AuthProvider');
  }

  return authContext;
};

As you can see, it is not the end of the world and doesn’t change much but I adore how the useAuth looks on the code. Also, I would like to create some other custom hooks related to auth context so that we don’t need to write the same code each time.

export const useName = () => {
  const { user } = useAuth();

  const [name, surname] = user?.nameSurname?.split(' ');

  const nameInitial = name.charAt(0);
  const surnameInitial = surname.charAt(0);

  const nameSurname = user?.nameSurname?.split(' ');
  const userName = nameInitial + surnameInitial;
  const fullName = nameSurname?.length >= 2 ? nameSurname?.[0] + ' ' + nameSurname?.[1] : nameSurname?.[0];

  return { userName, nameSurname, fullName };
};

export const useUserMail = () => {
  const { user } = useAuth();

  return { email: user?.email };
};

Let’s see what is happening here;

  1. useName is a custom hook that extracts the user's name and surname from the authentication context. It splits the nameSurname string into separate variables, captures the initials of the name and surname, and constructs a userName by concatenating the initials. It also creates a fullName by combining the first and second elements of the nameSurname array, or using the first element if there is no second element. The hook returns an object containing userName, nameSurname, and fullName.
  2. useUserMail is another custom hook that retrieves the user's email from the authentication context. It returns an object with a single property, email, which is set to the value of user?.email.

By using these hooks in our code, we can easily access and utilize the user's name, email, and related information from the authentication context without explicitly accessing the context or user object every time.

Now let’s see how we can use all these custom hooks and functions in our context;

const LoginForm = () => {
  const { loginUser } = useAuth();

  const onSubmit = (values: LoginFormValues) => {
    loginUser(values);
  };

  return (
    <Form
      onSubmit={onSubmit}
      render={({ handleSubmit }) => (
        <form onSubmit={handleSubmit}>
          <label htmlFor="email">
            <h3>Email Address</h3>
            <Field name="email">
              {({ input, meta }) => (
                <div>
                  <input {...input} id="email" type="email" placeholder="Email Address" />
                  {meta.error && meta.touched && <div className="error-message">{meta.error}</div>}
                </div>
              )}
            </Field>
          </label>

          <label htmlFor="password">
            <h3>Password</h3>
            <Field name="password">
              {({ input }) => (
                <div>
                  <input {...input} id="password" type="password" placeholder="Password" />
                </div>
              )}
            </Field>
          </label>
          <Button type="submit" variant="primary">
            Login
          </Button>
        </form>
      )}
    />
  );
};

As you can see, by only calling the loginUser function and connecting it to form submit with the necessary information we can use AuthContext in our project. The same way as the loginUser, we can also get user and fullName from our hooks like this to use in our code,

const { user } = useAuth();
const { fullName } = useName();

Additional Considerations

Creating an authentication context is a powerful technique for managing user authentication in your React projects. However, there are a few additional considerations you may want to keep in mind:

  1. Security: While this tutorial focuses on setting up the authentication context, it's crucial to prioritize security. Implement proper security measures, such as secure token storage, secure communication channels (HTTPS), and appropriate server-side validation and authorization checks.
  2. Error Handling: The code examples provided in this tutorial demonstrate the basic flow of authentication actions. However, it's essential to implement robust error handling mechanisms to handle various scenarios, such as network errors, server-side validation errors, and authentication failures. Ensure that you handle errors gracefully and provide meaningful feedback to your users.
  3. Remember Me Functionality: If your application requires a "Remember Me" functionality to keep users logged in across sessions, you'll need to consider additional implementation steps. This may involve storing long-lived tokens securely and providing mechanisms to manage session expiration and revocation.
  4. Two-Factor Authentication (2FA): If you want to enhance the security of your authentication system, consider integrating two-factor authentication. This can involve using SMS verification, email codes, or authenticator apps to add an extra layer of security for user authentication.

Conclusion

In this blog post, we explored the process of creating an authentication context in a React project. We learned how to set up the auth context, integrate authentication functionality, and consume the context in various components. By using the auth context, we can efficiently manage user authentication state, share data between components, and build secure and user-friendly authentication systems.

Remember to adapt the code examples to your specific project requirements and consider additional security measures and error handling strategies. Creating an authentication context empowers you to build robust and scalable authentication systems, ensuring a smooth and secure user experience.

With the knowledge gained from this tutorial, you are well-equipped to implement an auth context in your React projects. Embrace the power of React's auth context and elevate the security and functionality of your applications. Happy coding!

Share on

More Stories