Back to Medusa

{metadata.title}

www/apps/resources/app/storefront-development/regions/context/page.mdx

2.17.08.1 KB
Original Source

import { CodeTabs, CodeTab } from "docs-ui"

export const metadata = { title: Region React Context in Storefront, }

{metadata.title}

In this guide, you'll learn how to create a region context in your React storefront.

Why Create a Region Context?

Throughout your storefront, you'll need to access the customer's selected region to perform different actions, such as retrieve product's prices in the selected region.

<Note title="Tip">

To learn how to allow customers to select their region, refer to the Store Selected Region in Storefront guide.

</Note>

So, if your storefront is React-based, create a region context and add it at the top of your components tree. Then, you can access the selected region anywhere in your storefront.


Create Region Context Provider

For example, create the following file that exports a RegionProvider component and a useRegion hook:

<Note title="Tip">

Learn how to install and configure the JS SDK in the JS SDK documentation.

</Note>

export const highlights = [ ["13", "region", "Expose region to children of the context provider."], ["14", "setRegion", "Allow the context provider's children to change the selected region."], ["25", "RegionProvider", "The provider component to use in your component tree."], ["31", "", "If a region is set, set its ID in the local storage again in case it changed."], ["38", "regionId", "Retrieve the selected region from the localStorage."], ["41", "list", "If no region is selected, retrieve the list of regions from the Medusa application and select the first one."], ["47", "retrieve", "If a region is selected, retrieve it from the Medusa application."], ["64", "useRegion", "The hook that child components of the provider use to access the region."] ]

tsx
"use client" // include with Next.js 13+

import { 
  createContext, 
  useContext, 
  useEffect, 
  useState,
} from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"

type RegionContextType = {
  region?: HttpTypes.StoreRegion
  setRegion: React.Dispatch<
    React.SetStateAction<HttpTypes.StoreRegion | undefined>
  >
}

const RegionContext = createContext<RegionContextType | null>(null)

type RegionProviderProps = {
  children: React.ReactNode
}

export const RegionProvider = ({ children }: RegionProviderProps) => {
  const [region, setRegion] = useState<
    HttpTypes.StoreRegion
  >()

  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) {
      // retrieve regions and select the first one
      sdk.store.region.list()
      .then(({ regions }) => {
        setRegion(regions[0])
      })
    } else {
      // retrieve selected region
      sdk.store.region.retrieve(regionId)
      .then(({ region: dataRegion }) => {
        setRegion(dataRegion)
      })
    }
  }, [region])

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

export const useRegion = () => {
  const context = useContext(RegionContext)

  if (!context) {
    throw new Error("useRegion must be used within a RegionProvider")
  }

  return context
}

The RegionProvider handles retrieving the selected region from the Medusa application, and updating its ID in the localStorage.

The useRegion hook returns the value of the RegionContext. Child components of RegionProvider use this hook to access region or setRegion.


Use RegionProvider in Component Tree

To use the region context's value, add the RegionProvider high in your component tree.

For example, if you're using Next.js, add it to the app/layout.tsx or src/app/layout.tsx file:

tsx
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { CartProvider } from "@/providers/cart"
import { RegionProvider } from "@/providers/region"

const inter = Inter({ subsets: ["latin"] })

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <RegionProvider>
          {children}
        </RegionProvider>
      </body>
    </html>
  )
}

Use useRegion Hook

Now, you can use the useRegion hook in child components of RegionProvider.

For example:

tsx
"use client" // include with Next.js 13+
// ...
import { useRegion } from "@/providers/region"

export default function Products() {
  const { region } = useRegion()
  // ...
}

Managing Regions in Server Components

For SEO and performance reasons, many components in your storefront may be server components that don't have access to client-side context. Here are patterns for managing region data in server components:

URL-Based Region Selection

Store the selected region in the URL path or query parameters to make it available to server components:

tsx
import { sdk } from "@/lib/sdk"
import { cache } from "react"

// Server-side region retrieval
const getRegionByCountryCode = cache(async (countryCode: string) => {
  const { regions } = await sdk.store.region.list({
    fields: "*countries",
  })
  
  return regions.find((region) => 
    region.countries?.some((country) => 
      country.iso_2.toLowerCase() === countryCode.toLowerCase()
    )
  )
})

type Props = {
  params: { countryCode: string }
}

export default async function ProductsPage({ params }: Props) {
  const region = await getRegionByCountryCode(params.countryCode)
  
  if (!region) {
    // Handle region not found
    return <div>Region not found</div>
  }
  
  // Use region to fetch region-specific data
  const { products } = await sdk.store.product.list({
    region_id: region.id,
  })
  
  return (
    <div>
      <h1>Products for {region.name}</h1>
    </div>
  )
}

Store region preferences in cookies to access them in server components:

tsx
import { cookies } from "next/headers"
import { sdk } from "@/lib/sdk"

export async function getServerRegion() {
  const cookieStore = cookies()
  const regionId = cookieStore.get("region_id")?.value
  
  if (!regionId) {
    // Return default region
    const { regions } = await sdk.store.region.list({
      limit: 1,
    })
    return regions[0]
  }
  
  try {
    const { region } = await sdk.store.region.retrieve(regionId)
    return region
  } catch {
    // Fallback to default if region not found
    const { regions } = await sdk.store.region.list({
      limit: 1,
    })
    return regions[0]
  }
}

Then use it in server components:

tsx
import { getServerRegion } from "@/lib/region-server"
import { sdk } from "@/lib/sdk"

export default async function ServerProductList() {
  const region = await getServerRegion()
  
  const { products } = await sdk.store.product.list({
    region_id: region.id,
  })
  
  return (
    <div>
      {products.map((product) => (
        <div key={product.id}>
          <h3>{product.title}</h3>
        </div>
      ))}
    </div>
  )
}

Hybrid Approach

Combine server and client components for optimal performance and user experience:

tsx
import { Suspense } from "react"
import ServerProductList from "@/components/ServerProductList"
import ClientRegionSelector from "@/components/ClientRegionSelector"

export default function ProductsPage() {
  return (
    <div>
      <ClientRegionSelector />
      <Suspense fallback={<div>Loading products...</div>}>
        <ServerProductList />
      </Suspense>
    </div>
  )
}

This approach provides server-side rendering for SEO while maintaining client-side interactivity for region switching.