Cart Context and Provider
With the reducer in place, we can now build the Context layer. This involves two pieces: a Context object (the value components read) and a Provider component (the component that holds the state and makes it available).
Create the context
Create src/context/cart-context.tsx:
import { createContext } from "react";
import type { Product } from "@/api/types";
import type { CartItem } from "@/reducers/cart-reducer";
export type CartContextValue = {
items: CartItem[];
addToCart: (product: Product) => void;
removeFromCart: (productId: number) => void;
updateQuantity: (productId: number, quantity: number) => void;
clearCart: () => void;
totalItems: number;
totalPrice: number;
};
export const CartContext = createContext<CartContextValue | null>(null);
CartContextValue describes everything the cart makes available to consumers: the list of items, four action functions, and two computed totals. The context starts as null. Components that try to use it outside a provider will get null, which we will catch with a guard in the hook later.
Create the provider
Create src/providers/cart-provider.tsx:
import { useReducer } from "react";
import type { ReactNode } from "react";
import { cartReducer } from "@/reducers/cart-reducer";
import { CartContext } from "@/context/cart-context";
import type { Product } from "@/api/types";
export function CartProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const addToCart = (product: Product) => {
dispatch({ type: "added", product });
};
const removeFromCart = (productId: number) => {
dispatch({ type: "removed", productId });
};
const updateQuantity = (productId: number, quantity: number) => {
dispatch({ type: "quantity_updated", productId, quantity });
};
const clearCart = () => {
dispatch({ type: "cleared" });
};
const totalItems = state.items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = state.items.reduce(
(sum, item) => sum + item.product.price * item.quantity,
0,
);
return (
<CartContext
value={{
items: state.items,
addToCart,
removeFromCart,
updateQuantity,
clearCart,
totalItems,
totalPrice,
}}
>
{children}
</CartContext>
);
}
Here is what the provider does:
useReducermanages the cart state using the reducer from the previous section- Four convenience functions wrap
dispatchso consumers do not need to know about action types totalItemsandtotalPriceare computed from the current state on every render- All of this is passed through
CartContextto any descendant component
Notice that the reducer, the context, and the provider are each in their own file. The reducer manages the state logic, the context defines what is shared, and the provider connects the reducer to the context. Keeping them in separate files makes each one easier to understand, test, and reuse.
Checkpoint: Commit your progress.
git add .
git commit -m "shopping-cart-07: Cart context and provider"
git push