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.
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.
| Prop | Type | Default | Notes |
|---|---|---|---|
shop | string | — | Your store's domain, e.g. store.myshopify.com on Shopify — the identifier Product Genius resolves your configuration from. |
pageConfig | PDPConfig | 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. |
onAddToCart | OnAddToCart | — | Called when a recommendation card's Add-to-Cart is clicked. You own the cart mutation and the resulting UI. See Your onAddToCart. |
customerId | string | null | — | The logged-in customer id, if any (e.g. a Shopify customer id), for personalization. |
nonce | string | — | CSP nonce for the injected <script>. Omit if your CSP is purely host-allowlisted. See Content-Security-Policy. |
host | string | app.productgenius.io | The Product Genius host. Must be a bare hostname, optionally host:port. |
mountId | string | pg-app-wrapper | The DOM id the feed mounts into. A leading # is tolerated and stripped. See the mount node contract before changing it. |
className | string | — | Passed to the mount node — you own layout and placement. |
style | CSSProperties | — | 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".
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
onAddToCarton 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?