Use Context API to manage cart state
I would start by isolating your cart state and persistence to local storage to a react context provider. The context can provide the cart state and action dispatcher to the rest of the app, as well as persist the state to localStorage when the state updates using an effect. This decouples all of the state management from the app, the app need only consume the context to access the cart state and dispatch actions to update it.
import React, { createContext, useEffect, useReducer } from "react";
import { cartReducer, initializer } from "../cartReducer";
export const CartContext = createContext();
export const CartProvider = ({ children }) => {
const [cart, dispatch] = useReducer(cartReducer, [], initializer);
useEffect(() => {
localStorage.setItem("localCart", JSON.stringify(cart));
}, [cart]);
return (
<CartContext.Provider
value={{
cart,
dispatch
}}
>
{children}
</CartContext.Provider>
);
};
Wrap the app in the CartProvider
in index.js
<CartProvider>
<App />
</CartProvider>
Round out the rest of the app
In cartReducer
refine the reducer, and export the initializer function and action creators.
const initialState = [];
export const initializer = (initialValue = initialState) =>
JSON.parse(localStorage.getItem("localCart")) || initialValue;
export const cartReducer = (state, action) => {
switch (action.type) {
case "ADD_TO_CART":
return state.find((item) => item.name === action.item.name)
? state.map((item) =>
item.name === action.item.name
? {
...item,
quantity: item.quantity + 1
}
: item
)
: [...state, { ...action.item, quantity: 1 }];
case "REMOVE_FROM_CART":
return state.filter((item) => item.name !== action.item.name);
case "DECREMENT_QUANTITY":
// if quantity is 1 remove from cart, otherwise decrement quantity
return state.find((item) => item.name === action.item.name)?.quantity ===
1
? state.filter((item) => item.name !== action.item.name)
: state.map((item) =>
item.name === action.item.name
? {
...item,
quantity: item.quantity - 1
}
: item
);
case "CLEAR_CART":
return initialState;
default:
return state;
}
};
export const addToCart = (item) => ({
type: "ADD_TO_CART",
item
});
export const decrementItemQuantity = (item) => ({
type: "DECREMENT_QUANTITY",
item
});
export const removeFromCart = (item) => ({
type: "REMOVE_FROM_CART",
item
});
export const clearCart = () => ({
type: "CLEAR_CART"
});
In Product.js
get the cart context via a useContext
hook and dispatch an addToCart
action
import React, { useContext, useState } from "react";
import { CartContext } from "../CartProvider";
import { addToCart } from "../cartReducer";
const Item = () => {
const { dispatch } = useContext(CartContext);
...
const addToCartHandler = (product) => {
dispatch(addToCart(product));
};
...
return (
...
);
};
CartItem.js
get and use the cart context to dispatch actions to decrement quantity or remove item.
import React, { useContext } from "react";
import { CartContext } from "../CartProvider";
import { decrementItemQuantity, removeFromCart } from "../cartReducer";
const CartItem = () => {
const { cart, dispatch } = useContext(CartContext);
const removeFromCartHandler = (itemToRemove) =>
dispatch(removeFromCart(itemToRemove));
const decrementQuantity = (item) => dispatch(decrementItemQuantity(item));
return (
<>
{cart.map((item, idx) => (
<div className="cartItem" key={idx}>
<h3>{item.name}</h3>
<h5>
Quantity: {item.quantity}{" "}
<span>
<button type="button" onClick={() => decrementQuantity(item)}>
<i>Decrement</i>
</button>
</span>
</h5>
<h5>Cost: {item.cost} </h5>
<button onClick={() => removeFromCartHandler(item)}>Remove</button>
</div>
))}
</>
);
};
App.js
get both the cart state and dispatcher via context hook, and update the total items and price logic to account for item quantities.
import { CartContext } from "./CartProvider";
import { clearCart } from "./cartReducer";
export default function App() {
const { cart, dispatch } = useContext(CartContext);
const clearCartHandler = () => {
dispatch(clearCart());
};
const { items, total } = cart.reduce(
({ items, total }, { cost, quantity }) => ({
items: items + quantity,
total: total + quantity * cost
}),
{ items: 0, total: 0 }
);
return (
<div className="App">
<h1>Emoji Store</h1>
<div className="products">
<Product />
</div>
<div className="cart">
<CartItem />
</div>
<h3>
Items in Cart: {items} | Total Cost: ${total.toFixed(2)}
</h3>
<button onClick={clearCartHandler}>Clear Cart</button>
</div>
);
}