DEV Community

Cover image for πŸš€ Mastering Advanced Complex React useContext with useReducer ⭐ (Redux-like Style) ⭐
Salah Eddine Lalami for IDURAR | Where Ai Build Software

Posted on • Updated on • Originally published at idurarapp.com

πŸš€ Mastering Advanced Complex React useContext with useReducer ⭐ (Redux-like Style) ⭐

How to Create an Advanced Complex React Context with useReducer (Redux Style)

React Context API is a powerful feature that allows you to share data between components without having to pass props down the component tree. While it's great for simple use cases, managing complex state and actions can become challenging.

In this tutorial, we will explore how to create an advanced complex React Context using the useReducer hook, which provides a Redux-like approach to state management.

Prerequisites

To follow along with this tutorial, make sure you have a basic understanding of React and how to use React hooks. Familiarity with the concepts of Redux and its reducers will also be helpful.

Github Repo :

@ IDURAR we are using this approach in our open source ERP CRM

Github Repository : https://github.com/idurar/idurar-erp-crm

open source ERP CRM

Setting Up the Project

Before diving into the implementation details, let's set up our project. Start by creating a new React application using create-react-app. Open your terminal and run the following command:



npx create-react-app react-context-with-useReducer


Enter fullscreen mode Exit fullscreen mode

Once the project setup is complete, navigate into the project directory:



cd react-context-with-useReducer


Enter fullscreen mode Exit fullscreen mode

Creating the Context Provider

open source react app

Next, we'll create a new file called index.js inside a new directory named appContext. This file will contain our context provider component.



import React, { useMemo, useReducer, createContext, useContext } from 'react';
import { initialState, contextReducer } from './reducer';
import contextActions from './actions';

const AppContext = createContext();

function AppContextProvider({ children }) {
  const [state, dispatch] = useReducer(contextReducer, initialState);
  const value = useMemo(() => [state, dispatch], [state]);

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

function useAppContext() {
  const context = useContext(AppContext);
  if (context === undefined) {
    throw new Error('useAppContext must be used within a AppContextProvider');
  }
  const [state, dispatch] = context;
  const appContextAction = contextActions(dispatch);
  // const appContextSelector = contextSelectors(state);
  return { state, appContextAction };
}

export { AppContextProvider, useAppContext };



Enter fullscreen mode Exit fullscreen mode

In this code, we defines a React context called AppContext and provides a context provider component called AppContextProvider. It also includes a custom hook called useAppContext to access the context values. The context is initialized with a reducer and initial state. The AppContextProvider wraps around child components and provides the context value. The useAppContext hook allows accessing the state and actions related to the context. This setup enables sharing state and actions across different components in a React application.

Using the Context Provider

Now that we have our context provider, we can start using it in our application. Open the src/index.js file and wrap the root component with the AppProvider:



import React from 'react';
import ReactDOM from 'react-dom';

import App from './app';



import { AppContextProvider } from '@/context/appContext';

ReactDOM.render(
  <RouterHistory history={history}>

      <AppContextProvider>
        <App />
      </AppContextProvider>

  </RouterHistory>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();



Enter fullscreen mode Exit fullscreen mode

By wrapping our application with the AppProvider, all the components within the App component will have access to the context.

Create contextReducer

This code defines a Redux-like context reducer in JavaScript using React. Here are the comments explaining each section:



import * as actionTypes from './types';

// Define the initial state for the context
export const initialState = {
  isNavMenuClose: false,
};

// Define the reducer function for the context
export function contextReducer(state, action) {
  switch (action.type) {
    // Handle the OPEN_NAV_MENU action
    case actionTypes.OPEN_NAV_MENU:
      return {
        ...state,
        isNavMenuClose: false,
      };
    // Handle the CLOSE_NAV_MENU action
    case actionTypes.CLOSE_NAV_MENU:
      return {
        ...state,
        isNavMenuClose: true,
      };
    // Handle the COLLAPSE_NAV_MENU action
    case actionTypes.COLLAPSE_NAV_MENU:
      return {
        ...state,
        isNavMenuClose: !state.isNavMenuClose,
      };
    // Throw an error for any unhandled action types
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

Create context Actions

The code exports a function that provides an interface for dispatching context actions related to the navigation menu. This function can be used in React components to dispatch these actions and update the state of the context.



import * as actionTypes from './types';

// Define a function that returns an object with context actions
const contextActions = (dispatch) => {
  return {
    navMenu: {
      // Action for opening the navigation menu
      open: () => {
        dispatch({ type: actionTypes.OPEN_NAV_MENU });
      },
      // Action for closing the navigation menu
      close: () => {
        dispatch({ type: actionTypes.CLOSE_NAV_MENU });
      },
      // Action for toggling (collapsing/expanding) the navigation menu
      collapse: () => {
        dispatch({ type: actionTypes.COLLAPSE_NAV_MENU });
      },
    },
  };
};

export default contextActions;


Enter fullscreen mode Exit fullscreen mode

Accessing Context State and Dispatch

To access the state and dispatch function from our context, we need to use the useContext hook.here we demonstrate how to use the context:



import { useState, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Button, Drawer, Layout, Menu } from 'antd';

import { useAppContext } from '@/context/appContext';
import logoIcon from '@/style/images/logo-icon.svg';
import logoText from '@/style/images/logo-text.svg';


const SIDEBAR_MENU = [
  { key: '/', icon: <DashboardOutlined />, title: 'Dashboard' },
  { key: '/customer', icon: <CustomerServiceOutlined />, title: 'Customer' },
  { key: '/invoice', icon: <FileTextOutlined />, title: 'Invoice' },
  { key: '/quote', icon: <FileSyncOutlined />, title: 'Quote' },
  { key: '/payment/invoice', icon: <CreditCardOutlined />, title: 'Payment Invoice' },
  { key: '/employee', icon: <UserOutlined />, title: 'Employee' },
  { key: '/admin', icon: <TeamOutlined />, title: 'Admin' },
];

const SETTINGS_SUBMENU = [
  { key: '/settings', title: 'General Settings' },
  { key: '/payment/mode', title: 'Payment Mode' },
  { key: '/role', title: 'Role' },
];

const { Sider } = Layout;
const { SubMenu } = Menu;

export default function Navigation() {
  return (
    <>
      <div className="sidebar-wraper">
        <Sidebar collapsible={true} />
      </div>
      <MobileSidebar />
    </>
  );
}

function Sidebar({ collapsible }) {
  let location = useLocation();

  const { state: stateApp, appContextAction } = useAppContext();
  const { isNavMenuClose } = stateApp;
  const { navMenu } = appContextAction;
  const [showLogoApp, setLogoApp] = useState(isNavMenuClose);
  const [currentPath, setCurrentPath] = useState(location.pathname);

  useEffect(() => {
    if (location) if (currentPath !== location.pathname) setCurrentPath(location.pathname);
  }, [location, currentPath]);

  useEffect(() => {
    if (isNavMenuClose) {
      setLogoApp(isNavMenuClose);
    }
    const timer = setTimeout(() => {
      if (!isNavMenuClose) {
        setLogoApp(isNavMenuClose);
      }
    }, 200);
    return () => clearTimeout(timer);
  }, [isNavMenuClose]);
  const onCollapse = () => {
    navMenu.collapse();
  };

  return (
    <>
      <Sider
        collapsible={collapsible}
        collapsed={collapsible ? isNavMenuClose : collapsible}
        onCollapse={onCollapse}
        className="navigation"
      >
        <div className="logo" onClick={() => history.push('/')} style={{ cursor: 'pointer' }}>
          <img src={logoIcon} alt="Logo" style={{ height: '32px' }} />

          {!showLogoApp && (
            <img
              src={logoText}
              alt="Logo"
              style={{ marginTop: '3px', marginLeft: '10px', height: '29px' }}
            />
          )}
        </div>
        <Menu mode="inline" selectedKeys={[currentPath]}>
          {SIDEBAR_MENU.map((menuItem) => (
            <Menu.Item key={menuItem.key} icon={menuItem.icon}>
              <Link to={menuItem.key} />
              {menuItem.title}
            </Menu.Item>
          ))}
          <SubMenu key={'Settings'} icon={<SettingOutlined />} title={'Settings'}>
            {SETTINGS_SUBMENU.map((menuItem) => (
              <Menu.Item key={menuItem.key}>
                <Link to={menuItem.key} />
                {menuItem.title}
              </Menu.Item>
            ))}
          </SubMenu>
        </Menu>
      </Sider>
    </>
  );
}



Enter fullscreen mode Exit fullscreen mode

In the code above, we import the AppContext from our context file and use the useContext hook to access the state and dispatch function. From here, you can use the state and dispatch to update your component accordingly.

Updating the Context State with Actions

To update the state of our context, we need to define actions in our reducer function. Let's add an example action that increments a counter:

In this article, we learned how to create an advanced complex React Context using the useReducer hook. We set up a context provider, accessed the context state and dispatch function in our components, and updated the state using actions. This approach provides a Redux-like way of managing state within your React applications.

Github Repo :

@ IDURAR we are using this approach in our open source ERP CRM

Github Repository : https://github.com/idurar/idurar-erp-crm

open source ERP CRM

Top comments (5)

Collapse
 
silverium profile image
Soldeplata Saketos

AFAIK, all context will be updated, generating re-renders in the components inside the provider

Collapse
 
lalami profile image
Salah Eddine Lalami

Thank you @silverium for your comment,
We are using multiple context for each part of app , here are some techniques to avoid unnecessary re-renders when using useContext in React:

  1. Optimize the context value itself to update only when necessary.
  2. Split the context into multiple providers for more granular control.
  3. Avoid deep nesting of context consumers.
Collapse
 
mitchiemt11 profile image
Mitchell Mutandah

Nice article! Thanks for sharing.

Collapse
 
lalami profile image
Salah Eddine Lalami

u welcome

Collapse
 
lalami profile image
Salah Eddine Lalami

This was example of advanced use React Context , @ IDURAR , we use react context api for all UI parts , and we keep our data layer inside redux . any other suggestions ?