DEV Community

Cover image for React Context-API Pro | Build state management using useContext + useReducer | Typescript
Arkajit Roy
Arkajit Roy

Posted on

React Context-API Pro | Build state management using useContext + useReducer | Typescript

The Context API in React is a powerful feature that allows you to manage state globally across your application without the need to pass props down through multiple levels of your component tree. When combined with TypeScript, it becomes even more robust, providing type safety and better code maintainability. In this article, I'll walk you through setting up a Context API for authentication in a React app using modern conventions and TypeScript.

Project Structure

First, let's outline the project structure. We'll split our Context setup into four separate files:

context.tsx: This file will create and export the context.
provider.tsx: This file will provide the context to the component tree.
reducer.ts: This file will define the initial state and reducer function.
actions.ts: This file will contain action creators.
state.ts: This file will contain the initial state of the context
useAuthContext.ts: This file contain a custom hook which will help to call context

Image description

01. Creating the Context

We'll start by creating the context.tsx file. This file will initialize the context and provide a custom hook for easy access.

import { createContext } from "react";
import { AuthContextProps } from "../types";

export const AuthContext = createContext<AuthContextProps | undefined>(undefined);

Enter fullscreen mode Exit fullscreen mode

02. Defining the Initial State of the Context

After that we will define the initial state for the context in the state.ts. This initial state will be use for storing all the datas.

import { AuthState } from "../types";

export const initialState: AuthState = {
  isAuthenticated: false,
  user: null,
  token: null,
};
Enter fullscreen mode Exit fullscreen mode

03. Setting Up the Provider

Next, we'll set up the provider in the provider.tsx file. This component will use the useReducer hook to manage the state and pass it down via the context provider.

import React, { useReducer } from "react";
import { AuthProviderProps } from "../types";
import { AuthContext } from "./context";
import { AuthReducer } from "./reducer";
import { initialState } from "./state";

export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
  const [state, dispatch] = useReducer(AuthReducer, initialState);

  return <AuthContext.Provider value={{ state, dispatch }}>{children}</AuthContext.Provider>;
};
Enter fullscreen mode Exit fullscreen mode

04. Defining the Reducer

The reducer is the heart of our state management. In the reducer.ts file, we'll define our initial state and the reducer function to handle actions.

import { AuthAction, AuthState } from "../types";

export const AuthReducer = (state: AuthState, action: AuthAction): AuthState => {
  switch (action.type) {
    case "LOGIN":
      return {
        ...state,
        isAuthenticated: true,
        user: action.payload.user,
        token: action.payload.token,
      };
    case "LOGOUT":
      return {
        ...state,
        isAuthenticated: false,
        user: null,
        token: null,
      };
    default:
      return state;
  }
};
Enter fullscreen mode Exit fullscreen mode

05. Creating Actions

Action creators make it easier to dispatch actions in a type-safe way. We'll define these in the actions.ts file.

import { AuthAction, IUser } from "../types";

export const login = (user: IUser, token: string): AuthAction => {
  return {
    type: "LOGIN",
    payload: { user, token },
  };
};

export const logout = (): AuthAction => {
  return { type: "LOGOUT" };
};

Enter fullscreen mode Exit fullscreen mode

06. Configure the custom-hook.

This custom will help use set and call the context in any components without involving multiple parameters into it. Create a file and name it useAuthContext.ts.

import { useContext } from "react";
import { AuthContext } from "./context";

export const useAuthContext = () => {
  const context = useContext(AuthContext);
  if (!context) throw new Error("Error: useAuth must be within in AuthProvider");
  return context;
};

Enter fullscreen mode Exit fullscreen mode

We are set with all the initial configuration for the state management; now we will see how we can utilize this in our application.

Using the Context

To utilize our new AuthContext, we need to wrap our application (or part of it) in the AuthProvider. We'll do this in our main entry point, typically App.tsx.

import React from "react";
import { AuthProvider } from "./context/provider";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "./Home";

const App: React.FC = () => {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
        </Routes>
      </Router>
    </AuthProvider>
  );
};

export default App;


Enter fullscreen mode Exit fullscreen mode

Within any component, we can now use the useAuth hook to access the auth state and dispatch actions. Here's an example component that uses our AuthContext:


import React from "react";
import { useAuthContext } from "./context/useAuthContext";
import { IUser } from "./types";
import { login, logout } from "./context/actions";

const Home: React.FC = () => {
  const { state, dispatch } = useAuthContext();

  const handleLoginClick = () => {
    const user: IUser = { firstName: "John", lastName: "Doe", role: "SUPER-ADMIN" };
    const token = "dfs56ds56f5.65sdf564dsf.645sdfsd4f56";
    dispatch(login(user, token));
  };

  const handleLogoutClick = () => dispatch(logout());

  return (
    <div>
      <h1>Home Page</h1>
      {state.isAuthenticated ? (
        <div>
          <h3>Welcome {state.user?.firstName}</h3>
          <button onClick={handleLogoutClick}>Logout</button>
        </div>
      ) : (
        <button onClick={handleLoginClick}>Login</button>
      )}
    </div>
  );
};

export default Home;

Enter fullscreen mode Exit fullscreen mode

Types & Interfaces

import { Dispatch, ReactNode } from "react";

export interface IUser {
  firstName: string;
  lastName: string;
  role: "SUPER-ADMIN" | "ADMIN" | "USER";
}

export interface AuthState {
  isAuthenticated: boolean;
  user: null | IUser;
  token: null | string;
}

export interface AuthContextProps {
  state: AuthState;
  dispatch: Dispatch<AuthAction>;
}

export interface AuthProviderProps {
  children: ReactNode;
}

export type AuthAction =
  | { type: "LOGIN"; payload: { user: IUser; token: string } }
  | { type: "LOGOUT" };

Enter fullscreen mode Exit fullscreen mode

Conclusion

By following this structured approach, you can manage global state in your React applications more effectively. The Context API, when used with TypeScript, provides a powerful and type-safe solution for state management. This setup is not only limited to authentication but can be adapted for other use cases like theme management, language settings, and more.

With this knowledge, you can now use Context-API like a pro! Feel free to modify and extend this setup to fit the needs of your own projects.

Top comments (0)