Medusa React
Medusa React is a React library that provides a set of utilities and hooks for interacting seamlessly with the Medusa backend. It can be used to build custom React-based storefronts or admin dashboards.
Installation
In the directory holding your React-based storefront or admin dashboard, run the following command to install Medusa React:
In addition to the medusa-react
library, you need the following libraries:
1. @tanstack/react-query
: medusa-react
is built on top of Tanstack Query. You’ll learn later in this reference how you can use Mutations and Queries with Medusa React.
Versions of Medusa React prior to v4.0.2 used React Query v3 instead of Tanstack Query. Check out [this upgrade guide] to learn how you can update your storefront.
2. @medusajs/medusa
: The core Medusa package. This is used to import types used by Medusa React and while developing with it.
Part of the Medusa roadmap is to move the types into a separate package, removing the need to install the core Medusa package in your storefront or admin dashboard. You can check other items on our roadmap in GitHub Discussions.
Usage
To use the hooks exposed by Medusa React, you need to include the MedusaProvider
somewhere up in your component tree.
The MedusaProvider
requires two props:
baseUrl
: The URL to your Medusa backendqueryClientProviderProps
: An object used to set the Tanstack Query client. The object requires aclient
property, which should be an instance of QueryClient.
For example:
import { MedusaProvider } from "medusa-react"
import Storefront from "./Storefront"
import { QueryClient } from "@tanstack/react-query"
import React from "react"
const queryClient = new QueryClient()
const App = () => {
return (
<MedusaProvider
queryClientProviderProps={{ client: queryClient }}
baseUrl="http://localhost:9000"
>
<Storefront />
</MedusaProvider>
)
}
export default App
In the example above, you wrap the Storefront
component with the MedusaProvider
. Storefront
is assumed to be the top-level component of your storefront, but you can place MedusaProvider
at any point in your tree. Only children of MedusaProvider
can benefit from its hooks.
The Storefront
component and its child components can now use hooks exposed by Medusa React.
Troubleshooting: Could not find a declaration file for module 'medusa-react'
If you import medusa-react
in your code and see the following TypeScript error:
Make sure to set moduleResolution
in your tsconfig.json
to nodenext
or node
:
MedusaProvider Optional Props
You can also pass the following props to Medusa Provider:
Props | Default | Description |
---|---|---|
apiKey | '' | Optional API key used for authenticating admin requests. Follow this guide to learn how to create an API key for an admin user. |
publishableApiKey | '' | Optional publishable API key used for storefront requests. You can create a publishable API key either using the admin APIs or the Medusa admin. |
Queries
To fetch data from the Medusa backend (in other words, perform GET
requests), you can use Queries. Query hooks simply wrap around Tanstack Query's useQuery
hook to fetch data from your Medusa backend.
For example, to fetch products from your Medusa backend:
import { Product } from "@medusajs/medusa"
import { useProducts } from "medusa-react"
const Products = () => {
const { products, isLoading } = useProducts()
return isLoading ? (
<div>
Loading...
</div>
) : (
<ul>
{products?.map((product: Product) => (
<li key={product.id}>
{product.title}
</li>
))}
</ul>
)
}
export default Products
In the example above, you import the useProducts
hook from medusa-react
. This hook, and every other query hook exposed by medusa-react
, returns everything that useQuery
returns in Tanstack Query, except for the data
field.
Instead of the data
field, the response data is flattened and is part of the hooks’ returned fields. In the example above, the List Products API Route returns a products
array. So, useProducts
returns a products
array along with other fields returned by useQuery
.
If the request accepts any parameters, they can be passed as parameters to the mutate
request. For example:
You can learn more about using queries in Tanstack Query’s documentation.
Mutations
To create, update, or delete data on the Medusa backend (in other words, perform POST
, PUT
, and DELETE
requests), you can use Mutations. Mutation hooks wrap around Tanstack Query's useMutation
to mutate data on your Medusa backend.
For example, to create a cart:
import { useCreateCart } from "medusa-react"
const Cart = () => {
const createCart = useCreateCart()
const handleClick = () => {
createCart.mutate({}) // create an empty cart
}
return (
<div>
{createCart.isLoading && <div>Loading...</div>}
{!createCart.data?.cart && (
<button onClick={handleClick}>
Create cart
</button>
)}
{createCart.data?.cart?.id && (
<div>Cart ID: {createCart.data?.cart.id}</div>
)}
</div>
)
}
export default Cart
In the example above, you import the useCreateCart
hook from medusa-react
. This hook, and every other mutation hook exposed by medusa-react
, returns everything that useMutation returns. You can also pass the same options you would pass to useMutation
to mutation hooks exposed by medusa-react
.
To create a cart, you call the createCart.mutate
method. In the underlying logic, this method sends a POST
request to the Medusa backend to create a cart.
If the request accepts any parameters, they can be passed as parameters to the mutate
request. For example:
Once the cart is created, you can access it in the data
field returned by the mutation hook. This field includes all data returned in the response.
The example above does not store in the browser the ID of the cart created, so the cart’s data will be gone on release. You would have to do that using the browser’s Local Storage.
Instead of using mutate
, you can use mutateAsync
to receive a Promise that resolves on success or throws on error.
Learn more about how you can use mutations in Tanstack Query’s documentation.
Custom Hooks
Medusa React provides three utility hooks that allows developers to consume their admin custom API Routes using the same Medusa React methods and conventions.
useAdminCustomQuery
The useAdminCustomQuery
utility hook can be used to send a GET
request to a custom API Route in your Medusa backend and retrieve data. It's a generic function, so you can pass a type for the request and the response if you're using TypeScript in your development. The first type parameter is the type of the request body, and the second type parameter is the type of the expected response body:
The hook accepts the following parameters:
path
: (required) the first parameter is a string indicating the path of your API Route. For example, if you have custom API Routes that begin with/admin/vendors
, the value of this parameter would bevendors
. The/admin
prefix will be added automatically.queryKey
: (required) the second parameter is a string used to generate query keys, which are used by Tanstack Query for caching. When a mutation related to this same key succeeds, the key will be automatically invalidated.query
: (optional) the third parameter is an object that can be used to pass query parameters to the API Route. For example, if you want to pass anexpand
query parameter you can pass it within this object. Each query parameter's name is a key in the object. There are no limitations on what the type of the value can be, so you can pass an array or simply a string as a value.options
: (optional) the fourth parameter is an object of TanStack Query options.
The request returns an object containing keys like data
which is an object that includes the data returned in the response, and isLoading
which is a boolean value indicating whether the request is still in progress. You can learn more about the returned object's properties in TanStack Query's documentation.
For example:
import { useAdminCustomQuery } from "medusa-react"
import { useParams } from "react-router-dom"
type BlogPost = {
title: string,
content: string,
author_id: string,
}
// Single post
type AdminBlogPostQuery = {
expand?: string,
fields?: string
}
type AdminBlogPostRes = {
post: BlogPost,
}
const BlogPost = () => {
const { id } = useParams()
const { data, isLoading } = useAdminCustomQuery<
AdminBlogPostQuery,
AdminBlogPostRes
>(
`/blog/posts/${id}`, // path
["blog-post", id], // queryKey
{
expand: "author", // query
}
)
return (
<>
{isLoading && <span>Loading...</span>}
{data && data.post && <span>{data.post.title}</span>}
</>
)
}
export default BlogPost
import { useAdminCustomQuery } from "medusa-react"
import { useParams } from "react-router-dom"
type BlogPost = {
title: string,
content: string,
author_id: string,
}
type AdminBlogPostsRes = {
posts: BlogPost[],
count: number
}
type AdminBlogPostsQuery = {
author_id: string,
created_at?: string,
expand?: string,
fields?: string
}
const AuthorsBlogPosts = () => {
const { id } = useParams()
const { data, isLoading } = useAdminCustomQuery<
AdminBlogPostsQuery, AdminBlogPostsRes
>(
`/blog/posts`, // path
["blog-posts", "list", id], // queryKey
{
expand: "author", // query
author_id: "auth_123",
}
)
return (
<>
{isLoading && <span>Loading...</span>}
{data && data.posts && (
<span>
{data.posts.map((post, index) => (
<span key={index}>{post.title}</span>
))}
</span>
)}
</>
)
}
export default AuthorsBlogPosts
useAdminCustomPost
The useAdminCustomPost
utility hook can be used to send a POST
request to a custom API Route in your Medusa backend. It's a generic function, so you can pass a type for the request and the response if you're using TypeScript in your development. The first type parameter is the type of the request body, and the second type parameter is the type of the expected response body:
The hook accepts the following parameters:
path
: (required) the first parameter is a string indicating the path of your API Route. For example, if you have custom API Routes that begin with/admin/vendors
, the value of this parameter would bevendors
. The/admin
prefix will be added automatically.queryKey
: (required) the second parameter is a string used to generate query keys, which are used by Tanstack Query for caching. When the mutation succeeds, the key will be automatically invalidated.relatedDomains
: (optional) the third parameter is an object that can be used to specify domains related to this custom hook. This will ensure that Tanstack Query invalides the keys for those domains when your custom mutations succeed. For example, if your custom API Route is related to products, you can pass["products"]
as the value of this parameter. Then, when you use your custom mutation and it succeeds, the product's keyadminProductKeys.all
will be invalidated automatically, and all products will be re-fetched.options
: (optional) the fourth parameter is an object of Mutation options.
The request returns an object containing keys like mutation
which is a function that can be used to send the POST
request at a later point. You can learn more about the returned object's properties in TanStack Query's documentation.
For example:
import { useAdminCustomPost } from "medusa-react"
import { useNavigate } from "react-router-dom"
type BlogPost = {
id: string
title: string,
content: string,
author_id: string,
}
type AdminBlogPostReq = {
title: string,
content: string,
author_id: string,
}
type AdminBlogPostRes = {
post: BlogPost,
}
const CreateBlogPost = () => {
const navigate = useNavigate()
const { mutate, isLoading } = useAdminCustomPost<
AdminBlogPostReq,
AdminBlogPostRes
>(
`/blog/posts`,
["blog-posts"],
{
product: true,
}
)
const handleCreate = (args: AdminBlogPostReq) => {
return mutate(args, {
onSuccess: (data) => {
navigate(`blog/posts/${data.post.id}`)
},
})
}
// TODO replace with actual form
return (
<button
onClick={() => handleCreate({
title: "First Blog Post",
content: "Blog Content",
author_id: "auth_123",
})}>
Create
</button>
)
}
export default CreateBlogPost
useAdminCustomDelete
The useAdminCustomDelete
utility hook can be used to send a DELETE
request to a custom API Route in your Medusa backend. It's a generic function, so you can pass a type for the expected response if you're using TypeScript in your development:
The hook accepts the following parameters:
path
: (required) the first parameter is a string indicating the path of your API Route. For example, if you have custom API Routes that begin with/admin/vendors
, the value of this parameter would bevendors
. The/admin
prefix will be added automatically.queryKey
: (required) the second parameter is a string used to generate query keys, which are used by Tanstack Query for caching. When the mutation succeeds, the key will be automatically invalidated.relatedDomains
: (optional) the third parameter is an object that can be used to specify domains related to this custom hook. This will ensure that Tanstack Query invalides the keys for those domains when your custom mutations succeed. For example, if your custom API Route is related to products, you can pass["products"]
as the value of this parameter. Then, when you use your custom mutation and it succeeds, the product's keyadminProductKeys.all
will be invalidated automatically, and all products will be re-fetched.options
: (optional) the fourth parameter is an object of Mutation options.
The request returns an object containing keys like mutation
which is a function that can be used to send the DELETE
request at a later point. You can learn more about the returned object's properties in TanStack Query's documentation.
For example:
import { useAdminCustomDelete } from "medusa-react"
import { useNavigate, useParams } from "react-router-dom"
type AdminBlogPostDeleteRes = {
id: string,
type: string
}
const BlogPost = () => {
const { id } = useParams()
const navigate = useNavigate()
const { mutate, isLoading } = useAdminCustomDelete<
AdminBlogPostDeleteRes
>(
`/blog/posts/${id}`,
["blog-posts"],
{
product: true,
}
)
const handleDelete = () => {
return mutate(undefined, {
onSuccess: () => {
navigate("..")
},
})
}
// TODO replace with actual form
return (
<button
onClick={() => handleDelete()}>
Delete
</button>
)
}
export default BlogPost
Utilities
medusa-react
exposes a set of utility functions that are mainly used to retrieve or format the price of a product variant.
formatVariantPrice
This utility function can be used to compute the price of a variant for a region and retrieve the formatted amount. For example, $20.00
.
It accepts an object with the following properties:
variant
: A variant object retrieved from the Medusa backend. It should mainly include theprices
array in the object.region
: A region object retrieved from the Medusa backend.includeTaxes
: (optional) A boolean value that indicates whether the computed price should include taxes or not. The default value istrue
.minimumFractionDigits
: (optional) The minimum number of fraction digits to use when formatting the price. This is passed as an option toIntl.NumberFormat
in the underlying layer. You can learn more about this method’s options in MDN’s documentation.maximumFractionDigits
: (optional) The maximum number of fraction digits to use when formatting the price. This is passed as an option toIntl.NumberFormat
which is used within the utility method. You can learn more about this method’s options in MDN’s documentation.locale
: (optional) A string with a BCP 47 language tag. The default value isen-US
. This is passed as a first parameter toIntl.NumberFormat
which is used within the utility method. You can learn more about this method’s parameters in MDN’s documentation.
For example:
import { formatVariantPrice } from "medusa-react"
import { Product, ProductVariant } from "@medusajs/medusa"
const Products = () => {
// ...
return (
<ul>
{products?.map((product: Product) => (
<li key={product.id}>
{product.title}
<ul>
{product.variants.map((variant: ProductVariant) => (
<li key={variant.id}>
{formatVariantPrice({
variant,
region, // should be retrieved earlier
})}
</li>
))}
</ul>
</li>
))}
</ul>
)
}
computeVariantPrice
This utility function can be used to compute the price of a variant for a region and retrieve the amount without formatting. For example, 20
. This method is used by formatVariantPrice
before applying the price formatting.
It accepts an object with the following properties:
variant
: A variant object retrieved from the Medusa backend. It should mainly include theprices
array in the variant.region
: A region object retrieved from the Medusa backend.includeTaxes
: (optional) A boolean value that indicates whether the computed price should include taxes or not. The default value istrue
.
For example:
import { computeVariantPrice } from "medusa-react"
import { Product, ProductVariant } from "@medusajs/medusa"
const Products = () => {
// ...
return (
<ul>
{products?.map((product: Product) => (
<li key={product.id}>
{product.title}
<ul>
{product.variants.map((variant: ProductVariant) => (
<li key={variant.id}>
{computeVariantPrice({
variant,
region, // should be retrieved earlier
})}
</li>
))}
</ul>
</li>
))}
</ul>
)
}
formatAmount
This utility function can be used to compute the price of an amount for a region and retrieve the formatted amount. For example, $20.00
.
The main difference between this utility function and formatVariantPrice
is that you don’t need to pass a complete variant object. This can be used with any number.
It accepts an object with the following properties:
amount
: A number that should be used for computation.region
: A region object retrieved from the Medusa backend.includeTaxes
: (optional) A boolean value that indicates whether the computed price should include taxes or not. The default value istrue
.minimumFractionDigits
: (optional) The minimum number of fraction digits to use when formatting the price. This is passed as an option toIntl.NumberFormat
in the underlying layer. You can learn more about this method’s options in MDN’s documentation.maximumFractionDigits
: (optional) The maximum number of fraction digits to use when formatting the price. This is passed as an option toIntl.NumberFormat
which is used within the utility method. You can learn more about this method’s options in MDN’s documentation.locale
: (optional) A string with a BCP 47 language tag. The default value isen-US
. This is passed as a first parameter toIntl.NumberFormat
which is used within the utility method. You can learn more about this method’s parameters in MDN’s documentation.
For example:
computeAmount
This utility function can be used to compute the price of an amount for a region and retrieve the amount without formatting. For example, 20
. This method is used by formatAmount
before applying the price formatting.
The main difference between this utility function and computeVariantPrice
is that you don’t need to pass a complete variant object. This can be used with any number.
It accepts an object with the following properties:
amount
: A number that should be used for computation.region
: A region object retrieved from the Medusa backend.includeTaxes
: (optional) A boolean value that indicates whether the computed price should include taxes or not. The default value istrue
.
For example:
Content Providers
This is an experimental feature.
To facilitate building custom storefronts, medusa-react
also exposes a CartProvider
and a SessionCartProvider
.
CartProvider
CartProvider
makes use of some of the hooks already exposed by medusa-react
to perform cart operations on the Medusa backend. You can use it to create a cart, start the checkout flow, authorize payment sessions, and so on.
It also manages one single global piece of state which represents a cart, exactly like the one created on your Medusa backend.
To use CartProvider
, you first have to insert it somewhere in your component tree below the MedusaProvider
.
For example:
import { CartProvider, MedusaProvider } from "medusa-react"
import Storefront from "./Storefront"
import { QueryClient } from "@tanstack/react-query"
import React from "react"
const queryClient = new QueryClient()
function App() {
return (
<MedusaProvider
queryClientProviderProps={{ client: queryClient }}
baseUrl="http://localhost:9000"
>
<CartProvider>
<Storefront />
</CartProvider>
</MedusaProvider>
)
}
export default App
Then, in any of the child components, you can use the useCart
hook exposed by medusa-react
to get access to cart operations and data.
The useCart
hook returns an object with the following properties:
cart
: A state variable holding the cart object. This is set if thecreateCart
mutation is executed or ifsetCart
is manually used.setCart
: A state function used to set the cart object.totalItems
: The number of items in the cart.createCart
: A mutation used to create a cart.updateCart
: A mutation used to update a cart’s details such as region, customer email, shipping address, and more.startCheckout
: A mutation used to initialize payment sessions during checkout.pay
: A mutation used to select a payment processor during checkout.addShippingMethod
: A mutation used to add a shipping method to the cart during checkout.completeCheckout
: A mutation used to complete the cart and place the order.
For example:
import * as React from "react"
import { useCart } from "medusa-react"
const Cart = () => {
const handleClick = () => {
createCart.mutate({}) // create an empty cart
}
const { cart, createCart } = useCart()
return (
<div>
{createCart.isLoading && <div>Loading...</div>}
{!cart?.id && (
<button onClick={handleClick}>
Create cart
</button>
)}
{cart?.id && (
<div>Cart ID: {cart.id}</div>
)}
</div>
)
}
export default Cart
In the example above, you retrieve the createCart
mutation and cart
state object using the useCart
hook. If the cart
is not set, a button is shown. When the button is clicked, the createCart
mutation is executed, which interacts with the backend and creates a new cart.
After the cart is created, the cart
state variable is set and its ID is shown instead of the button.
The example above does not store in the browser the ID of the cart created, so the cart’s data will be gone on refresh. You would have to do that using the browser’s Local Storage.
SessionProvider
Unlike the CartProvider
, SessionProvider
never interacts with the Medusa backend. It can be used to implement the user experience related to managing a cart’s items. Its state variables are JavaScript objects living in the browser, but are in no way communicated with the backend.
You can use the SessionProvider
as a lightweight client-side cart functionality. It’s not stored in any database or on the Medusa backend.
To use SessionProvider
, you first have to insert it somewhere in your component tree below the MedusaProvider
.
For example:
import { SessionProvider, MedusaProvider } from "medusa-react"
import Storefront from "./Storefront"
import { QueryClient } from "@tanstack/react-query"
import React from "react"
const queryClient = new QueryClient()
const App = () => {
return (
<MedusaProvider
queryClientProviderProps={{ client: queryClient }}
baseUrl="http://localhost:9000"
>
<SessionProvider>
<Storefront />
</SessionProvider>
</MedusaProvider>
)
}
export default App
Then, in any of the child components, you can use the useSessionHook
hook exposed by medusa-react
to get access to client-side cart item functionalities.
For example: