Documentation Index
Fetch the complete documentation index at: https://mintlify.com/pieroenrico/tune-me-in/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The useProductsContext hook provides easy access to Shopify product data throughout your component tree. It’s designed to work with products fetched by useSanityQuery and made available through ProductsProvider.
This is particularly useful for accessing products in deeply nested components like Portable Text blocks and annotations without prop drilling.
Import
import {useProductsContext} from '../contexts/ProductsContext.client';
Basic Usage
const BlockInlineProduct = ({node}) => {
const product = node?.productWithVariant?.product;
const storefrontProduct = useProductsContext(product?._id);
if (!storefrontProduct) {
return '(Product not found)';
}
return (
<div>
<h3>{storefrontProduct.title}</h3>
<p>${storefrontProduct.priceRange.minVariantPrice.amount}</p>
</div>
);
};
Parameters
The Sanity product ID to retrieve from the context. This should match the _id format from your Sanity dataset (e.g., "shopifyProduct-7349334187288").
Return Value
Returns the Shopify product object for the specified ID, or undefined if the product is not found in the context.
The complete Shopify product data from the Storefront API, including:{
id: 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzY2Mzk2Mjk5MjY0ODc=',
title: 'Red T-shirt',
handle: 'red-tshirt',
descriptionHtml: '<p>Product description</p>',
priceRange: {
minVariantPrice: { amount: '29.99', currencyCode: 'USD' },
maxVariantPrice: { amount: '29.99', currencyCode: 'USD' }
},
compareAtPriceRange: { ... },
variants: { edges: [...] },
media: { edges: [...] },
metafields: { edges: [...] }
}
Setup
1. Fetch Products with useSanityQuery
First, fetch Shopify products in your server component:
src/pages/editorial/[handle].server.jsx
import {useSanityQuery} from 'hydrogen-plugin-sanity';
import ProductsProvider from '../../contexts/ProductsProvider.client';
export default function EditorialArticle() {
const {sanityData: sanityArticle, shopifyProducts} = useSanityQuery({
query: QUERY,
params: {slug: handle},
});
return (
<ProductsProvider value={shopifyProducts}>
<article>
<h1>{sanityArticle.title}</h1>
<PortableText blocks={sanityArticle.body} />
</article>
</ProductsProvider>
);
}
2. Access Products in Nested Components
Use the hook in any component within the provider:
src/components/blocks/BlockInlineProduct.client.jsx
import {useProductsContext} from '../../contexts/ProductsContext.client';
const BlockInlineProduct = ({node}) => {
const product = node?.productWithVariant?.product;
const storefrontProduct = useProductsContext(product?._id);
if (!storefrontProduct) {
return null;
}
return (
<Product product={storefrontProduct}>
<Product.Title />
<Product.Price />
</Product>
);
};
Usage Examples
Inline Product in Portable Text
src/components/blocks/BlockInlineProduct.client.jsx
import {Product} from '@shopify/hydrogen/client';
import {useProductsContext} from '../../contexts/ProductsContext.client';
import LinkProduct from '../LinkProduct.client';
const BlockInlineProduct = ({node}) => {
const product = node?.productWithVariant?.product;
const storefrontProduct = useProductsContext(product?._id);
if (!storefrontProduct) {
return '(Product not found)';
}
return (
<Product
product={storefrontProduct}
initialVariantId={product?.variantId}
>
<LinkProduct
handle={storefrontProduct.handle}
variantId={product?.variantId}
>
<Product.Title className="font-medium text-blue-500" />
</LinkProduct>
</Product>
);
};
export default BlockInlineProduct;
src/components/annotations/AnnotationProduct.client.jsx
import {Product} from '@shopify/hydrogen/client';
import Tippy from '@tippyjs/react/headless';
import {useProductsContext} from '../../contexts/ProductsContext.client';
const AnnotationProduct = ({children, mark}) => {
const product = mark?.productWithVariant?.product;
const storefrontProduct = useProductsContext(product?._id);
// Return text only if no valid product is found
if (!storefrontProduct) {
return children;
}
return (
<Tippy
interactive
placement="top"
render={(attrs) => (
<Product product={storefrontProduct}>
<div className="bg-white border border-black p-2" {...attrs}>
<Product.Title className="font-medium" />
<Product.Price />
<Product.SelectedVariant.Image className="my-2 w-full" />
</div>
</Product>
)}
>
<span className="text-blue-500 cursor-pointer">
{children}
</span>
</Tippy>
);
};
export default AnnotationProduct;
Margin Product Display
src/components/blocks/BlockInlineProductMarginalia.client.jsx
import {Product} from '@shopify/hydrogen/client';
import {useProductsContext} from '../../contexts/ProductsContext.client';
import ButtonSelectedVariantAddToCart from '../ButtonSelectedVariantAddToCart.client';
const BlockInlineProductMarginalia = ({node}) => {
const product = node?.productWithVariant?.product;
const storefrontProduct = useProductsContext(product?._id);
// Return nothing if no valid product is found
if (!storefrontProduct) {
return null;
}
return (
<Product
product={storefrontProduct}
initialVariantId={product?.variantId}
>
<div className="border p-4">
<Product.SelectedVariant.Image
options={{
width: 300,
height: 250,
crop: 'center',
}}
/>
<Product.Title className="text-lg font-medium mt-2" />
<Product.Price className="text-gray-600" />
<ButtonSelectedVariantAddToCart />
</div>
</Product>
);
};
export default BlockInlineProductMarginalia;
Error Handling
The hook throws an error if used outside of a ProductsProvider:
export const useProductsContext = (productId) => {
const context = useContext(ProductsContext);
if (!context) {
throw new Error('No products context found');
}
return context?.[productId];
};
Best practice: Always check if the returned product exists before using it:
const storefrontProduct = useProductsContext(product?._id);
if (!storefrontProduct) {
return null; // or a fallback UI
}
// Safe to use storefrontProduct here
ProductsProvider
The ProductsProvider component wraps your content and provides product data to the context.
Props
The shopifyProducts object returned by useSanityQuery. This should be a normalized object with product IDs as keys.{
'shopifyProduct-123': { /* product data */ },
'shopifyProduct-456': { /* product data */ }
}
The components that need access to product data.
Example
import ProductsProvider from '../../contexts/ProductsProvider.client';
<ProductsProvider value={shopifyProducts}>
<YourContent />
</ProductsProvider>
Type Safety
The context is defined using React’s createContext:
src/contexts/ProductsContext.client.jsx
import {createContext, useContext} from 'react';
const ProductsContext = createContext();
export default ProductsContext;
export const useProductsContext = (productId) => {
const context = useContext(ProductsContext);
if (!context) {
throw new Error('No products context found');
}
return context?.[productId];
};
Common Patterns
Pattern 1: Optional Product Reference
const storefrontProduct = useProductsContext(product?._id);
if (!storefrontProduct) {
return <div>Product unavailable</div>;
}
return <ProductDisplay product={storefrontProduct} />;
Pattern 2: Multiple Products
const products = productIds.map(id => useProductsContext(id)).filter(Boolean);
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
Pattern 3: Conditional Rendering
const storefrontProduct = useProductsContext(product?._id);
return storefrontProduct ? (
<ProductWithActions product={storefrontProduct} />
) : (
<span>{children}</span>
);