Product Genius Docs

ProductGeniusFeed

Render the Product Genius recommendation feed on your product page — the render example, every prop, and the two gotchas that bite.

<ProductGeniusFeed> is the whole storefront-side integration for most stores. It loads Product Genius on the page, seeds the product context a custom frontend doesn't publish on its own, registers your cart handler, and renders the mount node the feed fills in.

Render it on your product page wherever you want the recommendations to appear — commonly directly below the product details. The component renders into a single mount node, so you own its placement and layout through the surrounding markup and the className / style props.

Product page
import {ProductGeniusFeed} from '@productgenius/react';

function ProductRecommendations({product, selectedVariant}) {
  if (!selectedVariant) return null;

  return (
    <ProductGeniusFeed
      key={product.id} // SPA storefronts: remount per product (see below)
      shop="your-store.myshopify.com"
      pageConfig={{
        id: product.id, // gid or numeric — normalized for you
        vendor: product.vendor,
        variant: {
          id: selectedVariant.id,
          price: selectedVariant.price,
          sku: selectedVariant.sku,
        },
      }}
      onAddToCart={({variantId, quantity}) => {
        // Add the variant to YOUR cart — see "Your onAddToCart".
      }}
    />
  );
}

Where product and selectedVariant come from — a loader, a Server Component, a client-side fetch — depends on your framework; see the framework guides.

The component is client-only: it renders an empty mount node on the server (if you server-render at all), then on the client loads the feed and renders the recommendations into it.

Props

shop is the only required prop.

PropTypeDefaultNotes
shopstring—Your store's domain, e.g. store.myshopify.com on Shopify — the identifier Product Genius resolves your configuration from.
pageConfigPDPConfig | null—Configures the feed for the page it loads on: the product and selected variant from your loader, which anchors recommendations to that product. Omit or pass null on non-product routes.
onAddToCartOnAddToCart—Called when a recommendation card's Add-to-Cart is clicked. You own the cart mutation and the resulting UI. See Your onAddToCart.
customerIdstring | null—The logged-in customer id, if any (e.g. a Shopify customer id), for personalization.
noncestring—CSP nonce for the injected <script>. Omit if your CSP is purely host-allowlisted. See Content-Security-Policy.
hoststringapp.productgenius.ioThe Product Genius host. Must be a bare hostname, optionally host:port.
mountIdstringpg-app-wrapperThe DOM id the feed mounts into. A leading # is tolerated and stripped. See the mount node contract before changing it.
classNamestring—Passed to the mount node — you own layout and placement.
styleCSSProperties—Passed to the mount node.

Types

interface PDPConfig {
  /** Product id — a Shopify gid or a numeric id (normalized for you). */
  id: string;
  vendor?: string | null;
  /** The currently selected variant. Required for the feed to treat the page as
   *  a product page — without a resolved variant there is no product context. */
  variant?: {id: string; price?: string | null; sku?: string | null} | null;
}

interface AddToCartItem {
  /** Raw Shopify variant id from the feed (numeric). */
  variantId: string;
  quantity: number;
}

type OnAddToCart = (item: AddToCartItem) => void | Promise<void>;

pageConfig is optional by design. On non-product routes (collections, home, search), omit it and the page is treated as non-product: the feed still loads and renders wherever your store's Product Genius configuration allows it to. pageConfig anchors recommendations to a product; it does not gate rendering.

The mount node

The component renders exactly one <div>, with id="pg-app-wrapper" by default. That id is a contract: it matches the <div id="pg-app-wrapper"> that the Product Genius Shopify Liquid block renders, so a Liquid theme and a React storefront present the same mount point to the same bundle. See the Shopify section for the Liquid side.

The bundle finds its mount node using the container selector in your store's Product Genius configuration, so only change mountId if that configuration is changed to match — otherwise the feed has nowhere to render and your mount node stays empty.

One feed per page

<ProductGeniusFeed> is a page-level singleton: it owns the injected script and the registered cart handler. Rendering a second instance on the same page is not supported — in development it logs a warning, and the second instance does not re-register your handler. Render it once per page, in the one place you want recommendations.

If you pass a host that isn't a bare hostname (a path, a query string, an embedded protocol), the component refuses to load the script and logs an error in development, rather than letting a malformed value redirect the script origin.

Two gotchas

Remounting on SPA navigation

Pass key={product.id} on SPA storefronts

The feed fetches its recommendations once per mount. A client-side product → product navigation re-renders the same route without remounting it, so without a key the feed keeps showing the previous product's recommendations. key={product.id} forces a remount, which refetches.

This is the single most common integration bug, and it only shows up on client-side navigation — a full page load always looks correct. Verify it explicitly: navigate product → collection → a different product and confirm the recommendations changed.

Relatedly, the feed reads the product context when it initializes. If a shopper switches variants on a page where the feed has already loaded, the component re-seeds the context but the already-running feed does not refetch for the new variant. Add-to-Cart still adds the correct variant, because each card carries its own variant id.

Vite dev servers

Pre-bundle the package in Vite

Add the package to optimizeDeps.include so Vite doesn't discover it mid-load and re-optimize — that can crash hydration on the first npm run dev page load with a duplicate-React "Invalid hook call".

vite.config.ts
optimizeDeps: {include: ['@productgenius/react']},

This applies to every Vite-based setup, including Hydrogen: Shopify Hydrogen, React Router in framework mode, TanStack Start, and plain Vite + React. It affects the dev server only, but it fails in a way that looks like a bug in your app rather than a missing config line, so add it up front. Next.js doesn't use Vite and doesn't need it.

Handler updates and teardown

Two behaviours worth knowing when you review the diff:

  • You can pass a fresh inline arrow for onAddToCart on every render. The component reads the latest handler through a ref, so re-rendering never re-injects the script or re-registers the handler.
  • On unmount, the instance that injected the script removes it, deletes the registered cart handler, and tears the feed down. The effect is idempotent and React StrictMode-safe, so development's mount → unmount → mount leaves exactly one script and one handler.

Theming

The feed's appearance — design tokens and style overrides — is configured for your store on the Product Genius side, managed with your Product Genius contact. There is no styling work in your storefront beyond placing the mount node where you want it. (The search bar skeleton is the one exception: its pre-handoff appearance is yours to style.)

How is this guide?

On this page