4. React context

Add region and cart React context providers.

On this page

In your storefront, you'll need to access the following resources across components:

  • The selected region. In Aiminaabee, a region represents one or more countries you serve customers in. Each region can have different settings, such as currency codes, prices, payment providers, and more. You create regions in the Aiminaabee vendor dashboard, and you can allow the customer to choose their region in the storefront.
  • The customer's cart, which you'll use to add the product to purchase, set shipping and payment details, and create an order.

To easily access and manage these resources, you'll wrap your storefront with two React context providers: RegionProvider and CartProvider. You'll implement these providers in this step.

Create Region Provider

To create the region provider, create the file providers/region.tsx with the following content:

Project directory tree showing the newly created 'providers' folder containing the 'region.tsx' file, which implements React context for managing regional settings and currency selection across the storefront

tsx

tsxCopy
"use client"
import {
createContext,
} from "react"
import { HttpTypes } from "@aiminaabeejs/types"
type RegionContextType = {
region?: HttpTypes.StoreRegion
regions: HttpTypes.StoreRegion[]
setRegion: React.Dispatch<
React.SetStateAction<HttpTypes.StoreRegion | undefined>
>
}
const RegionContext = createContext<RegionContextType | null>(null)
// TODO add provider component

So far, you've created RegionContextType which represents the data that the provider's children can access. It holds:

  • region: The selected region.
  • regions: A list of regions available in the Aiminaabee application.
  • setRegion: A function to set the selected region.

Next, you'll create the provider component that wraps the storefront with the region context. Add the following content to the file:

tsx

tsxCopy
// other imports...
import {
// other imports...
useState,
} from "react"
// ...
type RegionProviderProps = {
children: React.ReactNode
}
export const RegionProvider = (
{ children }: RegionProviderProps
) => {
const [regions, setRegions] = useState<
HttpTypes.StoreRegion[]
>([])
const [region, setRegion] = useState<
HttpTypes.StoreRegion
>()
// TODO fetch regions
}

You create a RegionProvider component that so far only defines two state variables:

  • regions: A state variable that will hold the list of regions available in the Aiminaabee application.
  • region: A state variable that will hold the selected region.

Next, add the following imports to the top of the file:

tsx

tsxCopy
import {
// other imports...
useEffect,
} from "react"
import { sdk } from "@/lib/sdk"

And replace the TODO in the RegionProvider component with the following content:

]

tsx

tsxCopy
useEffect(() => {
if (regions.length) {
return
}
sdk.store.region.list("us")
.then(({ regions }) => {
setRegions(regions)
})
}, [])
// TODO set the selected region

When the component mounts, you fetch the regions from the Aiminaabee application using the JS SDK. You then set the regions in the regions state variable.

Next, replace the new TODO with the following content:

tsx

tsxCopy
useEffect(() => {
if (region) {
// set its ID in the local storage in
// case it changed
localStorage.setItem("region_id", region.id)
return
}
const regionId = localStorage.getItem("region_id")
if (!regionId) {
if (regions.length) {
setRegion(regions[0])
}
} else {
// retrieve selected region
sdk.store.region.retrieve("us", regionId)
.then(({ region: dataRegion }) => {
setRegion(dataRegion)
})
}
}, [region, regions])
// TODO return the provider

You add another useEffect hook that sets the selected region when the component first mounts and whenever the region changes. If the region is set, you only set its value in the local storage.

If the region isn't set in the state variable, you try to retrieve the previously selected value from the local storage. If it's not found, you set the first region in the regions list as the selected region. Otherwise, you retrieve the previously selected region from the Aiminaabee application using the JS SDK.

Finally, replace the last TODO with the following content:

tsx

tsxCopy
return (
<RegionContext.Provider value={{
region,
regions,
setRegion,
}}>
{children}
</RegionContext.Provider>
)

This returns the RegionContext.Provider component that will wraps the storefront with the region context, allowing its children to access the region data.

To simplify the usage of the region context in child components, add to the file the following custom hook that returns the region context:

tsx

tsxCopy
// other imports...
import {
// other imports...
useContext,
} from "react"
// ...
export const useRegion = () => {
const context = useContext(RegionContext)
if (!context) {
throw new Error("useRegion must be used within a RegionProvider")
}
return context
}

You export a useRegion hook that returns the region context. You'll use this hook later in child components to access the region.

Create Cart Provider

Next, you'll create the cart provider that allows you to access the customer's cart across components. Create the file providers/cart.tsx with the following content:

Directory structure after creating cart provider file

tsx

tsxCopy
"use client"
import {
createContext,
} from "react"
import { HttpTypes } from "@aiminaabeejs/types"
type CartContextType = {
cart?: HttpTypes.StoreCart
addToCart: (variantId: string, quantity: number) => Promise<
HttpTypes.StoreCart
>
updateCart: (data: {
updateData?: HttpTypes.StoreUpdateCart,
shippingMethodData?: HttpTypes.StoreAddCartShippingMethods
}) => Promise<HttpTypes.StoreCart | undefined>
refreshCart: () => Promise<HttpTypes.StoreCart | undefined>
updateItemQuantity: (itemId: string, quantity: number) => Promise<
HttpTypes.StoreCart
>
unsetCart: () => void
}
const CartContext = createContext<CartContextType | null>(null)
// TODO add provider component

You've created CartContextType that represents the data that the provider's children can access. It holds:

  • cart: The customer's cart. This will be set when the customer adds the product variant to the cart, and unset when the cart is completed.
  • addToCart: A function to add a product variant to the cart.
  • updateCart: A function to update the cart with new details, including shipping details.
  • refreshCart: A function to create a new cart and set it in the context.
  • updateItemQuantity: A function to update the quantity of an item in the cart.
  • unsetCart: A function to unset the cart from local storage and state variable.

Next, you'll create the provider component that wraps the storefront with the cart context. Add the following content to the file:

tsx

tsxCopy
// other imports...
import {
// other imports...
useState,
} from "react"
import { useRegion } from "./region"
// ...
type CartProviderProps = {
children: React.ReactNode
}
export const CartProvider = ({ children }: CartProviderProps) => {
const [cart, setCart] = useState<
HttpTypes.StoreCart
>()
const { region } = useRegion()
// TODO set cart
}

You create a CartProvider component that defines two state variables:

  • cart state variable that will hold the customer's cart details.
  • region holding the selected region retrieved from the region context.

Next, you'll add the logic to set the customer's cart in the provider. Add the following imports to the top of the file:

tsx

tsxCopy
import {
// other imports...
useEffect,
} from "react"
import { sdk } from "@/lib/sdk"

And replace the TODO in the CartProvider component with the following content:

tsx

tsxCopy
useEffect(() => {
if (!region) {
return
}
if (cart) {
localStorage.setItem("cart_id", cart.id)
return
}
const cartId = localStorage.getItem("cart_id")
if (!cartId) {
// create a cart
refreshCart()
} else {
// retrieve cart
sdk.store.cart.retrieve("us", cartId, {
fields:
"+items.variant.*,+items.variant.options.*,+items.product.options.*",
})
.then(({ cart: dataCart }) => {
setCart(dataCart)
})
}
}, [cart, region])
// TODO update cart when region changes

You add a useEffect hook that runs when the component mounts and whenever the cart is changed. If the cart is already set, you only set its value in the local storage.

If the cart isn't set, you either try to retrieve the cart set in local storage, or create a new cart.

Next, you'll add the logic to update the cart when the region changes. Replace the TODO with the following:

]

tsx

tsxCopy
useEffect(() => {
if (!cart || !region || cart.region_id === region.id) {
return
}
sdk.store.cart.update("us", cart.id, {
region_id: region.id,
})
.then(({ cart: dataCart }) => {
setCart(dataCart)
})
}, [region])
// TODO add functions

This useEffect hook runs when the region changes. If the cart is set and the region of the cart doesn't match the selected region, you update the cart with the new selected region.

You'll now implement the functions that are part of the context's value. Start by adding the refreshCart function in the place of the TODO:

tsx

tsxCopy
const refreshCart = async () => {
if (!region) {
return
}
const { cart: dataCart } = await sdk.store.cart.create("us", {
region_id: region.id,
})
localStorage.setItem("cart_id", dataCart.id)
setCart(dataCart)
return dataCart
}
// TODO add addToCart function

In the refreshCart function, you create a new cart in the selected region using the JS SDK and set it in the context. You also set the cart ID in the local storage, and return the cart.

Next, add the addToCart function in the place of the TODO:

tsx

tsxCopy
const addToCart = async (variantId: string, quantity: number) => {
const newCart = await refreshCart()
if (!newCart) {
throw new Error("Could not create cart")
}
const { cart: dataCart } = await sdk.store.cart.createLineItem("us", newCart.id, {
variant_id: variantId,
quantity,
})
setCart(dataCart)
return dataCart
}
// TODO add updateCart function

In the addToCart function, you use the refreshCart function to create a new cart, then add a product variant to the cart with the specified quantity using the JS SDK. You set the updated cart in the context and return the cart.

After that, replace the TODO with the updateCart function:

tsx

tsxCopy
const updateCart = async ({
updateData,
shippingMethodData,
}: {
updateData?: HttpTypes.StoreUpdateCart,
shippingMethodData?: HttpTypes.StoreAddCartShippingMethods
}) => {
if (!updateData && !shippingMethodData) {
return cart
}
let returnedCart = cart
if (updateData) {
returnedCart = (await sdk.store.cart.update("us", cart!.id, updateData)).cart
}
if (shippingMethodData) {
returnedCart = (await sdk.store.cart.addShippingMethod("us", cart!.id, shippingMethodData)).cart
}
setCart(returnedCart)
return returnedCart
}
// TODO add updateItemQuantity function

In the updateCart function, you update the cart's general details and set the cart's shipping method using the JS SDK. You then set the updated cart in the context and return the cart.

Then, replace the TODO with the updateItemQuantity function:

tsx

tsxCopy
const updateItemQuantity = async (itemId: string, quantity: number) => {
const { cart: dataCart } = await sdk.store.cart.updateLineItem("us",
cart!.id,
itemId,
{
quantity,
}
)
setCart(dataCart)
return dataCart
}
// TODO add unsetCart function

In the updateItemQuantity function, you update the quantity of an item in the cart using the JS SDK and set the updated cart in the context.

Lastly, replace the TODO with the unsetCart function:

tsx

tsxCopy
const unsetCart = () => {
localStorage.removeItem("cart_id")
setCart(undefined)
}
// TODO return provider

In the unsetCart function, you remove the cart ID from the local storage and unset the cart in the context.

Finally, you'll return the provider component. Replace the last TODO with the following content:

tsx

tsxCopy
return (
<CartContext.Provider value={{
cart,
addToCart,
updateCart,
refreshCart,
updateItemQuantity,
unsetCart,
}}>
{children}
</CartContext.Provider>
)

This returns the CartContext.Provider component that wraps the storefront with the cart context, allowing its children to access the cart data.

To simplify the usage of the cart context in child components, add in the same file the following custom hook that returns the cart context:

tsx

tsxCopy
// other imports...
import {
useContext,
} from "react"
// ...
export const useCart = () => {
const context = useContext(CartContext)
if (!context) {
throw new Error("useCart must be used within a CartProvider")
}
return context
}

You export a useCart hook that returns the cart context. You'll use this hook later in child components to access the cart.


Next: 5. Storefront layout