Simplifying React State Management: A Deep Dive into Context API vs Redux

In React, state management is crucial for building dynamic and scalable applications. As the size of an application grows, so does the complexity of handling state and sharing it between components. Two of the most popular solutions for managing state in React are the Context API and Redux. In this blog post, we'll dive deep into both solutions, which tackle the same problem but in very different ways.

Understanding State Management

State management in React is a fundamental concept that involves managing and sharing data across different components within an application. "State" refers to the data that determines how components render and behave. This data is often passed from parent components to child components using props. However, as our application grows in size and the number of components increases, passing props through multiple layers of components can become challenging to manage. This is known as prop drilling.

To solve this problem, state management solutions like the Context API and Redux are commonly used. These tools help streamline the process of managing and sharing state across your application, making it easier to maintain and scale.

Context API

The Context API is a built-in feature in React that facilitates sharing state or other data between components without the need to pass props down the component tree. Instead of manually passing props from one component to another, the Context API allows you to make data globally accessible across your application as context. This context can be accessed by any component that requires the data, regardless of its position in the component tree.

Core Components of Context API

  • Context: An API given to us by React, which allows us to pass the information to child components without using props. It is created using the createContext method of React.

  • Context Provider(Context.Provider): Context provider is created to wrap the components tree inside which we are going to access the context data. It takes a value prop that holds the state or data we want to share.

  • useContext: It is a React hook that is used inside the functional components. It is used inside the components to access the context that is passed using the provider to the components.

Setting Up Context

Setting up context involves 3 main steps creating context, creating context provider, and consuming the context using the useContext hook.

  • Creating the Context: We use createContext() method of React to create a context.

      import { createContext, useReducer } from "react";
    
      const initialState = {}
      const myExampleContext = createContext(initialState);
    
  • Creating the Context.Provider(): Wrapper function which will be used to wrap the component tree where context will be accessed. We can also pass the Reducer inside the context provider. Reducer provides us with functions that are used to manipulate the state.

      export const ContextProvider = ({children}) => {
          const [state, dispatch] = useReducer(myExampleReducer, initialState);
          const value = {}
          return (
              <myExampleContext.Provider value={value}>
                  {children}
              </myExampleContext.Provider>
          )
      }
    

    ContextProvider is a provider function wrapping the component that will be accessed as children. myExampleContext.Provider is the actual context wrapper which is wrapping the children we got as an argument from the provider function. The value object is the actual final data which will be accessible to the components.

  • Consuming the Context: For this, either we can directly access the context in each component along with the useContext hook and then access it or we can create a function that will help us directly access the context data just by using that function.

      export const useContextData = () => {
          return useContext(myExampleContext);
      }
    

Use cases of Context API

  • For small to medium sized applications where minimal state management is there, we can stick to the built-in solution provided by react without the hassle of installing state management libraries separately.

  • When we have to manage static or semi-static data like themes, user authentication data, localization etc. we can go with Context API for state management.

Advantages of Context API

  • Built-In and Lightweight: Context API is a built-in feature of React and does not require installing or setting up any external libraries.

  • No Boilerplate Code: Unlike some state management solutions, the Context API doesn’t require a lot of boilerplate code. You can set up and use context with just a few lines of code, making it quick and efficient to implement.

  • Performance: It is best used in cases where the state is static and doesn’t change frequently. Because each component that consumes the context will re-render if there is any change in context. So, it needs to be managed carefully.

Disadvantages of Context API

  • Lacks Built-In Structure: The Context API does not provide the structured approach that Redux offers. When managing state across a large application with many components, multiple contexts are often required, leading to more complexity and potential for errors.

  • No DevTools Support: Unlike Redux,which provides us Redux DevTools, the Context API does not offer an equivalent debugging solution. This makes it more difficult to inspect, track, and debug changes to the context’s state.

  • Custom Solution Required: For anything beyond simple state sharing we need to write a custom solution with additional logic that can make the codebase harder to manage.

Redux and Redux Toolkit

Redux is a popular JavaScript library developed for predictable and maintainable global state management. It allows us to manage the application's state from a single central location, known as the store. This centralized store makes it easy for various components across your application to access and interact with the state consistently.

Since all the redux logic has to be manually hand-written it was prone to errors and bugs, so Redux Toolkit was created to eliminate this problem, it provides us with a reduced boilerplate code removing the hand-written redux logic and providing APIs that simplify the standard redux task.

Core Concepts of Redux

To effectively work with Redux, it's crucial to have an understanding of its three core concepts: Store, Actions, and Reducers.

  • Store: The Store in Redux is the central hub where our application state is stored. It acts as a warehouse where all inventory for our application is stored. It is created using the configureStore method provided by the Redux Toolkit.

      import { configureStore } from "@reduxjs/toolkit";
    
      export const store = configureStore({
          reducer:{}
      });
    
  • Actions: Actions are plain javascript objects that are used to send data from the application to the redux store. Actions are dispatched to the redux store using the dispatch function indicating that something has happened. It should have a type property indicating the type of change and a payload property having the data sent from the application.

      const addAction = {
          type: 'ADD',
          payload: 1
        };
    
  • Reducers: Reducers are pure Javascript functions that decide how the state of the application changes based on the action received from the application. They take the current state and action as an argument and return the updated state.

      function stateReducers(state = { sum: 0 }, action) {
          switch (action.type) {
            case 'ADD':
              return { ...state, sum: state.sum + action.payload };
            default:
              return state;
          }
        }
    

Setting up Redux Store for our Application

Consider we have a cart functionality in our application and we need to manage the state of the cart globally using Redux with two reducer functions add and remove product.

  • Creating the Slice Object: Slice object created to define how the state can be updated. It has all the Redux Reducer logic and initial state defined inside a single file. Created using the createSlice method provided by the Redux toolkit, which takes an object as an argument inside which we have to pass a name property defining the name for the slice, initial state object, and the reducer object having all the reducer functions.

      const cartSlice = createSlice({
          name: 'cart',
          initialState: {
              cartList: [],
              total: 0
          },
          reducers: {
              add(state,actions){
                  //reducer function to add product to cart
              },
              remove(state,actions){
                  //reducer function to remove product from cart
              }
          }
      });
      // exporting the add and remove function from slice
      export const {add,remove}  = cartSlice.actions;
    
      // this reducer will be accessed and registered in the store
      export const cartReducer = cartSlice.reducer;
    
  • Setting up the Store: Redux toolkit provides us with a function configureStore which helps in store setup and automatically integrates it with the Redux devtools.

      import { combineReducers, configureStore } from "@reduxjs/toolkit";
      import { cartReducer } from "./cartSlice";
    
      const rootReducer = combineReducers({
          cartReducer: cartReducer
      })
      export const store = configureStore({
          reducer: rootReducer
      });
    

    combineReducer is a function provided by Redux Toolkit to combine and manage reducers declared in different slices we have created. Making it easier for us to manage and scale our applications state management.

  • Wrapping the Component using the Provider from Redux: Wrapping the components using the Provider given to us by Redux, so that we can pass the store the component tree that will be accessing it. Here in the example we have a small cart application so we are wrapping the whole application.

      import React from 'react';
      import ReactDOM from 'react-dom/client';
      import { Provider } from 'react-redux';
      import { store } from './store/store';
      import App from './App';
      import './index.css';
    
      const root = ReactDOM.createRoot(document.getElementById('root'));
      root.render(
        <React.StrictMode>
          <Provider store={store}>
             <App />
          </Provider>
        </React.StrictMode>
      );
    
  • Accessing state and sending actions from Component using Dispatch: We can directly access the actions declared in the slice and use the useDispatch hook from the redux to access the dispatch function and send the action to the store.

      import { useDispatch } from 'react-redux';
      import {remove} from '../store/cartSlice.js';
      import './CartCard.css';
    
      export const CartCard = ({product}) => {
        const dispatch = useDispatch();
        return (
          <div className="cart-card">
              <img src={product.image} alt="" />
              <p className='name'>{product.name}</p>
              <p className='price'>${product.price}</p>  
              <button onClick={() => dispatch(remove(product))}>Remove</button>
          </div>
        )
      }
    

Use Cases of Redux

  • When we have an app with a large codebase and complex state management, Redux provides us the structure and scalability for our application.

  • Works fine with static state and also dynamic state when the state is constantly changing.

Advantages of Redux

  • Predictable State Updates: State updates in Redux are controlled with the help of actions and reducers. It makes it easier to track, test and debug changes in state over time.

  • Middleware Support: Redux provides support for middleware like Redux Thunk or Redux Saga that allow us to handle complex functions like handling asynchronous operation, logging and interacting with APIs.

  • Redux Devtools: Redux devtools extension provides great debugging capabilities. It allows us to inspect state, actions and even revert to previous state making it easier to work with large scale applications.

Disadvantages of Redux

  • Mannual State Updates: In Redux immutability is followed that is reducer functions updating the state must always return a new state object which is good for predictability but can become error prone when trying to manage the state manually.

  • Boilerplate Code: We have to define action, action types, reducers in slices and then configure the store which results in a significant amount of boilerplate code even for simple tasks

  • Global State Updates: In Redux, every action dispatch causes the root reducer to run, even if only a small part of the state changes. This overhead can slow down performance, especially in large applications with many state updates.

Conclusion

We can see that both Context API and Redux are powerful tools for state management with each having their own use cases. Context API is a lightweight solution better when we are developing a simpler application with static state data whereas Redux is more suitable for large and complex applications handling dynamic state with predictable updates. By understanding the advantages and disadvantages of each we can choose the one solution that suits us better.

13
Subscribe to my newsletter

Read articles from Abhishek Sadhwani directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Abhishek Sadhwani
Abhishek Sadhwani