Managing Global State in React.js Apps: A Practical Approach

leiniercs

Leinier

Posted on October 5, 2024

Managing Global State in React.js Apps: A Practical Approach

Introduction

When developing any application, be it console, desktop, mobile, or web, efficient data management is crucial. While in languages like C++ we can resort to modules with functions to read and write global variables, ensuring data integrity in asynchronous environments, in the world of web applications with React.js, the dynamic nature of the environment demands a different approach.

In React.js, handling global variables, especially mutable ones that act as a “global state,” requires a particular treatment. This article explores this challenge, sharing my experience and learning on how to build high-quality, robust React.js applications through effective global state management.

The following is a brief example of how to handle a state variable in a component function in an application created with Next.js where we will display a list of products and allow the user to add the products of their preference to the shopping cart:

// [/app/page.tsx]

"use client";
import type { Product } from "@/data/products";
import { useCallback, useState } from "react";
import { products } from "@/data/products";

export default function Page() {
   const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
   const addProductToCart = useCallback((index: number) => {
      setSelectedProducts((prevState: number[]) => prevState.concat([index]));
   }, []);

   return (
      <div className="flex flex-col gap-10">
         <div className="flex flex-wrap gap-10">
            {products.map((product: Product, index: number) => (
               <div
                  key={index}
                  className="flex flex-col gap-2 justify-between overflow-hidden w-44 p-4 border-2 rounded-lg"
               >
                  <h1>Product {product.name}</h1>
                  <div className="flex justify-between">
                     <span>${product.price}</span>
                     <button
                        className="border-1 rounded-lg"
                        onClick={() => addProductToCart(index)}
                     >
                        Add to cart
                     </button>
                  </div>
               </div>
            ))}
         </div>
         <div className="flex flex-col gap-4">
            <h1>Cart:</h1>
            {selectedProducts.map((index: number) => (
               <span key={index}>
                  Product {products[index].name}. ${products[index].price}
               </span>
            ))}
         </div>
      </div>
   );
}
Enter fullscreen mode Exit fullscreen mode

The above code snippet is sufficient to handle the task of displaying a list of products and adding them to the shopping cart if the user decides to do so, but it is limited to the context of a component function. What if we want other component functions to reuse the list of products added to the cart? To achieve this, we need to use a technique that allows us to use a common point accessible by all the component functions of our application.

Creating a global state manager

React.js offers an alternative method of managing state variables, which can be used in a more efficient way than "useState" and from a global perspective: "useReducer". The use of "useReducer" combined with "useContext" gives us the tools and ways to create a global variable management module in our React.js application.

I begin to illustrate how to build this module by creating a component that will serve as a base to achieve the desired goal:

// [/components/contexts/global.tsx]

"use client";
import type { ReactNode, Context } from "react";
import { createContext, useCallback, useEffect, useReducer } from "react";

interface CustomComponentProps {
   children: ReactNode;
}

export interface Product {
   name: string;
   price: number;
}

enum Action {
   SET_PRODUCTS = 0x01,
   CART_ADD_PRODUCT,
   CART_REMOVE_PRODUCT
}

interface CustomContextProps {
   products: Product[];
   productsInCart: number[];
   setProducts(products: Product[]): void;
   addProductToCart(index: number): void;
   removeProductFromCart(index: number): void;
}

interface CustomDispatchProps {
   action: Action;
   value: number | Product[];
}

let _initialStates: CustomContextProps = {
   products: [],
   productsInCart: [],
   setProducts: () => {},
   addProductToCart: () => {},
   removeProductFromCart: () => {}
};

function statesReducer(
   states: CustomContextProps,
   data: CustomDispatchProps
): CustomContextProps {
   switch (data.action) {
      case Action.SET_PRODUCTS:
         return {
            ...states,
            products: data.value as Product[]
         };
      case Action.CART_ADD_PRODUCT:
         return {
            ...states,
            productsInCart: states.productsInCart.concat([data.value as number])
         };
      case Action.CART_REMOVE_PRODUCT:
         return {
            ...states,
            productsInCart: states.productsInCart.filter(
               (productIndex: number) => (data.value as number) !== productIndex
            )
         };
      default:
         return states;
   }
}

export const GlobalContext: Context<CustomContextProps> =
   createContext<CustomContextProps>(_initialStates);

export function GlobalProvider({
   children
}: Readonly<CustomComponentProps>): ReactNode {
   const [{ products, productsInCart }, dispatch] = useReducer(
      statesReducer,
      _initialStates
   );
   const setProducts = useCallback(
      (products: Product[]) =>
         dispatch({
            action: Action.SET_PRODUCTS,
            value: products
         }),
      []
   );
   const addProductToCart = useCallback(
      (index: number) =>
         dispatch({
            action: Action.CART_ADD_PRODUCT,
            value: index
         }),
      []
   );
   const removeProductFromCart = useCallback(
      (index: number) =>
         dispatch({
            action: Action.CART_REMOVE_PRODUCT,
            value: index
         }),
      []
   );

   _initialStates = {
      products,
      productsInCart,
      setProducts,
      addProductToCart,
      removeProductFromCart
   };

   useEffect(() => {
      setProducts([
         { name: "A", price: 1 },
         { name: "B", price: 2 },
         { name: "C", price: 3 }
      ]);
   }, [setProducts]);

   return (
      <GlobalContext.Provider value={_initialStates}>
         {children}
      </GlobalContext.Provider>
   );
}
Enter fullscreen mode Exit fullscreen mode

The above file defines all the logic for handling global scope variables that will be used throughout all the component functions of our application. The bases that allow handling global variables internally and the public functions that will allow mutating said variables were defined.

Below I illustrate how to successfully integrate through the different files that make up our application.

// [/app/layout.tsx]

import { GlobalProvider } from "@/components/contexts/global";
import "./globals.css";

interface CustomComponentProps {
   children: React.ReactNode;
}

export default function RootLayout({
   children
}: Readonly<CustomComponentProps>) {
   return (
      <html lang="en">
         <body className="antialiased">
            <GlobalProvider>{children}</GlobalProvider>
         </body>
      </html>
   );
}
Enter fullscreen mode Exit fullscreen mode

In the previous code, the provider component for the custom context that was created in the global state variables module is inserted into the main layout component of our application in order to initialize it and make it available to the entire application.

// [/app/page.tsx]

import { Products } from "@/components/home/products";
import { Cart } from "@/components/home/cart";

export default function Page() {
   return (
      <div className="flex flex-col gap-10">
         <Products />
         <Cart />
      </div>
   );
}
Enter fullscreen mode Exit fullscreen mode

In the main page it is streamed the client components that will show the products and cart list.

// [/components/home/products.tsx]

"use client";
import type { Product } from "@/components/contexts/global";
import { useContext } from "react";
import { GlobalContext } from "@/components/contexts/global";

export function Products() {
   const { products, addProductToCart } = useContext(GlobalContext);

   return (
      <div className="flex flex-wrap gap-10">
         {products.map((product: Product, index: number) => (
            <div
               key={index}
               className="flex flex-col gap-2 justify-between overflow-hidden w-44 p-4 border-2 rounded-lg"
            >
               <h1>Product {product.name}</h1>
               <div className="flex justify-between">
                  <span>${product.price}</span>
                  <button
                     className="border-1 rounded-lg"
                     onClick={() => addProductToCart(index)}
                  >
                     Add to cart
                  </button>
               </div>
            </div>
         ))}
      </div>
   );
}
Enter fullscreen mode Exit fullscreen mode

The previous code illustrates the Products component that uses the variables and functions that were defined in the GlobalContext context. In this case, the products list and the function that allows us to mutate that list in the global state variables module, which is common to other components.

// [/components/home/cart.tsx]

"use client";
import { useContext } from "react";
import { GlobalContext } from "@/components/contexts/global";

export function Cart() {
   const { products, productsInCart } = useContext(GlobalContext);

   return (
      <div className="flex flex-col gap-4">
         <h1>Cart:</h1>
         {productsInCart.map((productIndex: number, index: number) => (
            <span key={index}>
               Product {products[productIndex].name}. $
               {products[productIndex].price}
            </span>
         ))}
      </div>
   );
}
Enter fullscreen mode Exit fullscreen mode

The previous code access to the list of products added to the cart from the Products component.

Conclusion

The changes illustrated above result in the execution of a web application that stands out for the segmentation of the handling of global scope variables and the different component functions (pages in our web application) that access them and mutate them through the public functions exported from the global variables management module.

The above can serve as a basis for improving the code and obtaining data asynchronously, executing calculations based on the data obtained, among others that may be the result of the programmer's imagination.

Would this example be useful for your next web application with Next.js?

💖 💪 🙅 🚩
leiniercs
Leinier

Posted on October 5, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related