Introduction
The Context API in React allows sharing state across the entire app without prop drilling. useReducer handles complex state logic with a reducer function. Together, they offer a powerful way to manage global state predictably, reduce boilerplate code, and improve code organization. Additionally, they enhance performance by preventing unnecessary re-renders and make it easier to debug state changes.
Prerequisites
- Node.js and npm
- React
- Typescript
- MUI
Diving into Context API
The Context API offers a straightforward and effective method for sharing state throughout a React component tree. It addresses the issues of prop drilling, which can be messy and frustrating when passing props down to components.
With the Context API, you can pass props to any child component of a parent, enabling state sharing without the need to pass props through each level.
- Provider and Consumer
The Context API has two main components:
- Provider
- Consumer
The Provider is a React component that wraps other components, while the Consumer consumes the data or state passed through the Provider. It’s important to note that the Context API is ideal for passing global data. If your use case involves global state, it’s likely a suitable choice.
Diving into useReducer :
The useReducer hook enables the management of complex state logic in React components. If you’ve used Redux before, the concept of useReducer is pretty similar to Redux. A reducer is basically a switch statement defining all the possible actions a context can perform, and only updating the part of the global state related to that action.
- Syntax
const [state, dispatch] = useReducer(reducer, initialState)
The line const [state, dispatch] = useReducer(reducer, initialState); initializes state management using useReducer in React. Here, state holds the current state of the application, while dispatch is a function that triggers state updates by invoking the reducer function with an action, and initialState represents the initial state value for the application’s state management. It defines the starting point of the state before any actions are dispatched to update it.
- Advantages of useReducer
- When dealing with complex state logic that involves multiple sub-values or where the next state depends on the previous one, useReducer is typically a better choice than useState.
- Using useReducer can also help optimize component performance by allowing you to pass the dispatch function down instead of individual callbacks, which can reduce unnecessary re-renders.
A Comprehensive Guide to Application
This React + TypeScript application is a shopping app demonstrating the use of the Context API with useReducer for state management. By using the Context API along with useReducer, the state is efficiently shared and managed across components without the need for prop drilling. This setup showcases a clean and scalable approach to handling the global state in a React application.
By following the below steps, you can easily learn how to use context api with useReducer and also make an application where you leverage the advantages of context api.
Let’s understand the folder structure
- There is a folder for components, where all the necessary components for the application will be created.
- There is a folder named assets, where all the styling files, images will be stored.
- Then, a separate state folder is created for the context and reducer .
Therefore, it is good practice to follow a proper folder structure and separate our concerns.
Navigating the Frontend Setup In React
Let’s start the UI implementation for using React with a TypeScript template.
- Initializing React Application : To create a React application, you need to start by creating a project folder in your root directory. Inside the project folder, install the necessary dependencies by running the following commands.
// Create react app using the following cmd
npx create-react-app state-management --template typescript
- Please follow the below step to make the shopping app using context api and useReducer hook for state management. In the project state-management folder, locate the App.tsx file which serves as the entry point for the project.
1) Create a separate folder for the components where you will create the necessary components.
2) In that components folder, make a file named Products.tsx and CartModal.tsx.
3) Make a separate folder named state. In that folder make a file with the nameContext.tsx and reducer.ts.
4) Make a separate folder named assets. In that folder make another folder named images to store the images.
4) Make a separate folder named helper. In that folder make a file with the name ProductData.ts to store the dummy data for the products.
5) In this app, material ui is used for styling. Design the user interface according to your preference using raw CSS or any library of your choice.
//App.tsx
//Necessary imports
function App() {
return (
);
}
export default App;
- In the App component, the Products component is wrapped inside the ShopProvider, so all the child components will be able to use the context value.
//context.tsx
//Necessary imports
interface ShopContextProps {
total: number;
products: IProduct[];
addToCart: (product: IProduct) => void;
removeFromCart: (product: IProduct) => void;
}
export const ShopContext = createContext(undefined);
export const ShopProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(shopReducer, initialState);
const addToCart = (product: IProduct) => {
const updatedCart = [...state.products, product];
updatePrice(updatedCart);
dispatch({
type: "ADD_TO_CART",
payload: {
products: updatedCart,
},
});
};
const removeFromCart = (product: IProduct) => {
const updatedCart = state.products.filter(
(currentProduct) => currentProduct.id !== product.id
);
updatePrice(updatedCart);
dispatch({
type: "REMOVE_FROM_CART",
payload: {
products: updatedCart,
},
});
};
const updatePrice = (products: IProduct[]) => {
let total = 0;
products.forEach((product) => (total += parseFloat(product.price.slice(1))));
dispatch({
type: "UPDATE_PRICE",
payload: {
total,
},
});
};
const value = {
total: state.total,
products: state.products,
addToCart,
removeFromCart,
};
return {children} ;
};
- The ShopProvider sets up the context for managing the shopping cart state using the useReducer hook and the shopReducer function. It provides functions addToCart and removeFromCart to modify the cart and an updatePrice function to calculate the total price.
- The ShopContext is created and used to pass down these values and functions to the rest of the application. The provider wraps around the application’s children components to give them access to the cart state and functions.
//reducer.ts
export interface IProduct {
id: number;
name: string;
description: string;
price: string;
image: string;
}
interface InitialState {
total: number;
products: IProduct[];
}
export const initialState: InitialState = {
total: 0,
products: [],
};
interface AddToCartAction {
type: "ADD_TO_CART";
payload: {
products: IProduct[];
};
}
interface RemoveFromCartAction {
type: "REMOVE_FROM_CART";
payload: {
products: IProduct[];
};
}
interface UpdatePriceAction {
type: "UPDATE_PRICE";
payload: {
total: number;
};
}
export type Action = AddToCartAction | RemoveFromCartAction | UpdatePriceAction;
const shopReducer = (state: InitialState, action: Action): InitialState => {
switch (action.type) {
case "ADD_TO_CART":
return {
...state,
products: action.payload.products,
};
case "REMOVE_FROM_CART":
return {
...state,
products: action.payload.products,
};
case "UPDATE_PRICE":
return {
...state,
total: action.payload.total,
};
default:
return state;
}
};
export default shopReducer;
- The shopReducer function manages the state of the shopping cart in a Redux-like manner. It handles three types of actions: ADD_TO_CART, REMOVE_FROM_CART, and UPDATE_PRICE.
- When a product is added or removed from the cart, the corresponding action updates the products array in the state. The UPDATE_PRICE action updates the total price of the items in the cart. The initialState defines the initial structure of the state with a total of 0 and an empty products array.
//Products.tsx
//Necessary imports
export interface IProductData {
id: number;
name: string;
description: string;
price: string;
image: string;
}
const Products = () => {
const context = useContext(ShopContext);
if (!context) {
throw new Error("Error");
}
const { products, addToCart, removeFromCart, total } = context;
const [cartIsOpen, setCartIsOpen] = useState(false);
const isInCart = (product: IProductData) => {
return products.some((cartProduct) => cartProduct.id === product.id);
};
const handleClick = (product: IProductData) => {
if (isInCart(product)) {
removeFromCart(product);
} else {
addToCart(product);
}
};
const toggleCart = () => {
setCartIsOpen((cartIsOpen) => !cartIsOpen);
};
return (
<>
<>
Shopeasy
}
>
View Cart
Products
{ProductData.map((product) => (
{ Render Product }
))}
>
>
);
};
export default Products;
- The Products.tsx component displays a list of products and allows users to add or remove products from their shopping cart. It utilizes ShopContext to manage the state of the cart, including the functions addToCart and removeFromCart.
- The component also includes a VIEW CART button to toggle the visibility of the cart modal.
- Each product is displayed with its image, name, description, and price, and has a button to add or remove it from the cart.
//CartModal.tsx
//Necessary imports
interface ICartModalProps {
open: boolean;
onClose: () => void;
}
const CartModal: React.FC = ({ open, onClose }) => {
const context = useContext(ShopContext);
if (!context) {
throw new Error("Error");
}
const { products, total } = context;
const handleClose = () => {
onClose();
};
return (
);
};
export default CartModal;
- Here, in the CartModal component, we are using products and total from the ShopContext and then rendering the products added to the cart in the modal.
//ProductData.ts
import { IProductData } from "../components/Products";
export const ProductData: IProductData[] = [
{
id: 1,
name: "Shoes",
description: "Comfortable running shoes",
price: "$129.00",
image: shoes1,
},
// Add more dummy data
]
- The Product.ts stores the dummy data for the products which is used in the Products.tsx component to render the products.
Conclusion
In conclusion, creating a React TypeScript application using the Context API and the useReducer hook effectively manages the state. This approach centralizes state management, allowing for a clear and scalable structure. The Context API provides a way to pass down the state and actions without prop drilling. The useReducer hook simplifies complex state logic, making the code more maintainable and predictable.