Creating an authentication context with useContext in React

Creating an authentication context is a practical way to manage user authentication state across your entire React application. By using the Context API together with hooks like useContext and useState, you can easily share authentication state and logic across components.

There are several ways to pass information from parent components to child components in React. One way to do that is to use props. This works great with immediate children, but becomes more challenging when dealing with a deep component tree. A second way is to use the Context API to pass data deeply with Context. Context allows the parent to make information available to any component in the UI tree without using prop drilling.

Creating an authentication context

First we will use createContext to create our AuthContext. Create a file AuthContext.js somewhere in your project. I suggest you create a context folder at the same level of your App.js.

import React, { createContext, useContext, useState } from 'react';

const AuthContext = createContext();

export function useAuth() {
  return useContext(AuthContext);
}

Here we are initializing a new context and then we define and export useAuth(). This function defines and exports the custom hook so that we can easily access the context.

Next we need to declare an AuthProvider component that will wrap our application. We will also declare a login() and logout() methods. Our methods are simple for this demonstration. They will update the user appropriately.


export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Check if user is logged in when the app loads
    fetch('/api/check-auth')
      .then(response => response.json())
      .then(data => {
        setUser(data.user); // Adjust based on your API response
        setLoading(false);
      }).catch((e) => {
        // Navigate to the login page...
      });
  }, []);

  const login = (loggedInUser) => {
    setUser(loggedInUser);
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {!loading && children}
    </AuthContext.Provider>
  );
}

Our AuthProvider uses the useEffect hook to check authentication from our server. This is particular useful in single page applications when users refresh the browser on any other route that is not the root route. You may have a different way of maintaining your authentication session, for now we are going to assume that the server is responsible for doing so.

We are also waiting to load any children with this line{!loading && children}, until we have the server’s response. For now we are not rendering anything. This would be a good opportunity render something that will improve the user experience rather than a blank screen or “loading” text.

Integrating AuthProvider with our application

The integration is quite simple. We are going to wrap our App component with our new context.

import React from 'react';
import ReactDOM from 'react-dom';
import { AuthProvider } from './AuthContext';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <AuthProvider>
      <App />
    </AuthProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

Now we can use our authContext with any component. Here is an example of how you can create a login component and use it to update the authentication context.

import React, { useState } from 'react';
import { useAuth } from './AuthContext';

function LoginComponent() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const { login } = useAuth();

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const result = await login(email, password);
      // Redirect the user or update the UI
    } catch (error) {
      // Handle errors (e.g., show error message)
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Login</button>
    </form>
  );
}

You can import and use the context into any component: import { useAuth } from './AuthContext'. To get your current user all you have to do is call const { user } = useAuth() and you can now perform any business logic based on whether the user is logged in.

As a challenge create a ProtectedRoute component and use useAuth() to get the current user. If the user does not exist, navigate to the login page. Here is an example of how you would use it. See if you can implement it with react-router-dom.

<Routes>
  <ProtectedRoute>
    <Route path="/" element={
          <Dashboard />
      } />
  </ProtectedRoute>
  <Route path="/register" element={<Register />} /> 
  <Route path="/login" element={<Login />} />
</Routes>

Good luck!