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
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
Once the project setup is complete, navigate into the project directory:
cd react-context-with-useReducer
Creating the Context Provider
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 };
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();
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}`);
}
}
}
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;
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>
</>
);
}
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
Top comments (5)
AFAIK, all context will be updated, generating re-renders in the components inside the provider
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:
Nice article! Thanks for sharing.
u welcome
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 ?