7. Cart context

Create, retrieve, and share the cart across screens.

On this page

In this step, you'll create a cart context to manage the customer's cart state throughout the app. The cart context will allow you to create, manage, and complete the cart.

Create Cart Context

Create the file context/cart-context.tsx with the following content:

tsx

tsxCopy
import { sdk } from "@/lib/sdk"
import { FetchError } from "@aiminaabeejs/js-sdk"
import type { HttpTypes } from "@aiminaabeejs/types"
import AsyncStorage from "@react-native-async-storage/async-storage"
import React, { createContext, ReactNode, useCallback, useContext, useEffect, useState } from "react"
import { useRegion } from "./region-context"
interface CartContextType {
cart: HttpTypes.StoreCart | null;
addToCart: (variantId: string, quantity: number) => Promise<void>;
updateItemQuantity: (itemId: string, quantity: number) => Promise<void>;
removeItem: (itemId: string) => Promise<void>;
refreshCart: () => Promise<void>;
clearCart: () => Promise<void>;
loading: boolean;
error: string | null;
}
const CartContext = createContext<CartContextType | undefined>(undefined)
const CART_STORAGE_KEY = "cart_id"
export function CartProvider({ children }: { children: ReactNode }) {
const [cart, setCart] = useState<HttpTypes.StoreCart | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const { selectedRegion } = useRegion()
// TODO load cart
}
// TODO add useCart hook

You define the CartContext with the following properties that child components can use:

  • cart: The current cart object.
  • addToCart: A function to add an item to the cart.
  • updateItemQuantity: A function to update the quantity of an item in the cart.
  • removeItem: A function to remove an item from the cart.
  • refreshCart: A function to refresh the cart data from the backend.
  • clearCart: A function to clear the cart, which is useful after checkout.
  • loading: A boolean indicating whether a cart operation is in progress.
  • error: An error message if a cart operation fails.

You also define the CartProvider component that will wrap the app and provide the cart context to its children.

Next, you'll implement the logic to load the cart from the Aiminaabee backend. Replace the // TODO load cart comment with the following:

tsx

tsxCopy
const loadCart = useCallback(async () => {
if (!selectedRegion) {return null}
try {
setLoading(true)
setError(null)
const savedCartId = await AsyncStorage.getItem(CART_STORAGE_KEY)
if (savedCartId) {
try {
const { cart: fetchedCart } = await sdk.store.cart.retrieve("us", savedCartId, {
fields: "+items.*",
})
setCart(fetchedCart)
return fetchedCart
} catch {
// Cart not found or invalid, remove from storage
await AsyncStorage.removeItem(CART_STORAGE_KEY)
}
}
// Create new cart for current region
const { cart: newCart } = await sdk.store.cart.create("us", {
region_id: selectedRegion.id,
}, {
fields: "+items.*",
})
setCart(newCart)
await AsyncStorage.setItem(CART_STORAGE_KEY, newCart.id)
return newCart
} catch (err) {
setError(`Failed to load cart: ${err instanceof FetchError ? err.message : String(err)}`)
return null
} finally {
setLoading(false)
}
}, [selectedRegion])
// Load cart on mount
useEffect(() => {
loadCart()
}, [loadCart])
// TODO handle region update

The loadCart function creates or retrieves the saved cart from the Aiminaabee backend using the JS SDK. This function runs when the component mounts using the useEffect hook.

Next, you'll implement the logic to handle region updates, as the cart's region must match the selected region.

Replace the // TODO handle region update comment with the following:

tsx

tsxCopy
useEffect(() => {
const updateCartRegion = async () => {
if (!cart || !selectedRegion || cart.region_id === selectedRegion.id) {
return
}
try {
setLoading(true)
const { cart: updatedCart } = await sdk.store.cart.update("us", cart.id, {
region_id: selectedRegion.id,
}, {
fields: "+items.*",
})
setCart(updatedCart)
} catch (err) {
setError(`Failed to update cart region: ${err instanceof FetchError ? err.message : String(err)}`)
} finally {
setLoading(false)
}
}
updateCartRegion()
}, [selectedRegion])
// TODO implement cart operations

You add an effect that runs whenever the selected region changes. If the cart's region doesn't match the selected region, it updates the cart's region using the JS SDK.

Next, you'll implement the cart operations that you defined in the context type. Replace the // TODO implement cart operations comment with the following:

tsx

tsxCopy
const addToCart = async (variantId: string, quantity: number) => {
let currentCart = cart
if (!currentCart) {
currentCart = await loadCart()
if (!currentCart) {throw new Error("Could not create cart")}
}
try {
setLoading(true)
setError(null)
const { cart: updatedCart } = await sdk.store.cart.createLineItem("us", currentCart.id, {
variant_id: variantId,
quantity,
}, {
fields: "+items.*",
})
setCart(updatedCart)
} catch (err) {
setError(`Failed to add item to cart: ${err instanceof FetchError ? err.message : String(err)}`)
throw err
} finally {
setLoading(false)
}
}
const updateItemQuantity = async (itemId: string, quantity: number) => {
if (!cart) {return}
try {
setLoading(true)
setError(null)
const { cart: updatedCart } = await sdk.store.cart.updateLineItem("us",
cart.id,
itemId,
{ quantity },
{
fields: "+items.*",
}
)
setCart(updatedCart)
} catch (err) {
setError(`Failed to update quantity: ${err instanceof FetchError ? err.message : String(err)}`)
throw err
} finally {
setLoading(false)
}
}
const removeItem = async (itemId: string) => {
if (!cart) {return}
try {
setLoading(true)
setError(null)
const { parent: updatedCart } = await sdk.store.cart.deleteLineItem("us", cart.id, itemId, {
fields: "+items.*",
})
setCart(updatedCart!)
} catch (err) {
setError(`Failed to remove item: ${err instanceof FetchError ? err.message : String(err)}`)
throw err
} finally {
setLoading(false)
}
}
const refreshCart = async () => {
if (!cart) {return}
try {
const { cart: updatedCart } = await sdk.store.cart.retrieve("us", cart.id, {
fields: "+items.*",
})
setCart(updatedCart)
} catch (err) {
setError(`Failed to refresh cart: ${err instanceof FetchError ? err.message : String(err)}`)
}
}
const clearCart = async () => {
setCart(null)
await AsyncStorage.removeItem(CART_STORAGE_KEY)
// Create a new cart
if (selectedRegion) {
const { cart: newCart } = await sdk.store.cart.create("us", {
region_id: selectedRegion.id,
}, {
fields: "+items.*",
})
setCart(newCart)
await AsyncStorage.setItem(CART_STORAGE_KEY, newCart.id)
}
}
return (
<CartContext.Provider
value={{
cart,
addToCart,
updateItemQuantity,
removeItem,
refreshCart,
clearCart,
loading,
error,
}}
>
{children}
</CartContext.Provider>
)

You define the following cart operation functions:

  • addToCart: Adds an item to the cart by creating a line item.
  • updateItemQuantity: Updates the quantity of an item in the cart.
  • removeItem: Removes an item from the cart.
  • refreshCart: Refreshes the cart data from the backend.
  • clearCart: Clears the cart and creates a new one.

You also add a return statement providing the context values to child components using the CartContext.Provider.

Finally, you'll create a custom hook in the same file to access the cart context easily. Replace the // TODO add useCart hook comment with the following:

tsx

tsxCopy
export function useCart() {
const context = useContext(CartContext)
if (!context) {
throw new Error("useCart must be used within a CartProvider")
}
return context
}

The useCart hook retrieves the context value using useContext. It throws an error if used outside of a CartProvider.

Wrap App in Cart Provider

Next, you'll wrap the app in the CartProvider to provide the cart context to all components.

In app/_layout.tsx, add the following import at the top of the file:

tsx

tsxCopy
import { CartProvider } from "@/context/cart-context"

Then, in the RootLayout's return statement, add the CartProvider as a child of the RegionProvider and a parent of the Stack component:

tsx

tsxCopy
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<RegionProvider>
<CartProvider>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
</Stack>
<StatusBar style="auto" />
</CartProvider>
</RegionProvider>
</ThemeProvider>
</GestureHandlerRootView>
)

Child components can now access the cart context using the useCart hook.


Next: 8. Home screen