Skip to main content

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

productId
string
required
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.
product
object | undefined
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;

Product Annotation with Tooltip

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

value
object
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 */ }
}
children
ReactNode
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>
);