@planetdataset/sdk-hydrogen
v0.2.4
Published
React/Hydrogen wrapper for @planetdataset/sdk-core. Exports PlanetWidgetsBoot, PlanetBogo, PlanetBundle, PlanetUpsell, PlanetSmartCart, PlanetSmartCartModal, usePlanetSmartCart, planetCartApi, toLiquidCart.
Downloads
1,037
Readme
@planetdataset/sdk-hydrogen
React + Hydrogen wrapper around @planetdataset/sdk-core.
Exports:
- Components —
<PlanetWidgetsBoot>,<PlanetSmartCart>,<PlanetSmartCartModal>,<PlanetBogo>,<PlanetBundle>,<PlanetUpsell>,<PlanetCartRecommendations> - Hook —
usePlanetSmartCart() - Helpers —
planetCartApi,toLiquidCart,extractNumericId
Everything is SSR-safe: the underlying UMD is dynamically imported inside useEffect, so the server bundle never touches window.
Install
npm install @planetdataset/sdk-hydrogen@planetdataset/sdk-core is installed automatically as a transitive dependency — you don't need to add it to your package.json explicitly.
Peer dependencies: react ^18 || ^19, react-dom, @shopify/hydrogen.
Setup
1. Create a new file app/routes/cart[.]json.jsx
The SDK reads the merchant cart through fetch('/cart.json'). You need to create this file in your Hydrogen app — it doesn't exist by default. It exposes a Liquid-shaped JSON endpoint that translates Hydrogen's GraphQL cart into the format Planet widgets expect.
Paste the following content into app/routes/cart[.]json.jsx:
import {toLiquidCart} from '@planetdataset/sdk-hydrogen';
export async function loader({context}) {
const cart = await context.cart.get();
return Response.json(toLiquidCart(cart), {
headers: {'Cache-Control': 'no-store'},
});
}The bracketed filename cart[.]json.jsx is intentional — it tells the React Router file convention to expose the route at /cart.json (a literal dot).
2. Edit app/root.jsx — boot the SDK and expose loader data
Three things to do in app/root.jsx:
a) Add the loader data
In your existing loader() function, add a planet block to the returned object:
// inside app/root.jsx
// Planet widgets render every price with the merchant's Shopify money format
// string. The Storefront API exposes it on `shop.moneyFormat`.
const SHOP_MONEY_FORMAT_QUERY = `#graphql
query ShopMoneyFormat {
shop {
moneyFormat
}
}
`;
export async function loader(args) {
const {storefront, env} = args.context;
// ... your existing loader body ...
const {shop} = await storefront.query(SHOP_MONEY_FORMAT_QUERY);
return {
// ... your existing returned fields ...
planet: {
storeDomain: env.PUBLIC_STORE_DOMAIN,
publicAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN,
shopId: env.PUBLIC_SHOP_ID,
countryCode: storefront.i18n.country,
languageCode: storefront.i18n.language,
moneyFormat: shop.moneyFormat,
},
};
}This object MUST be returned from the loader — it's what <PlanetWidgetsBoot> reads via useRouteLoaderData('root').
Money format (currency display) —
moneyFormatis the Shopify money format string (${{amount}},€{{amount_with_comma_separator}},{{ amount }} kr…) the widgets use to render every price. Fetching it fromshop.moneyFormatkeeps widget prices matching your storefront's currency symbol, separators, and decimals. Skip it and<PlanetWidgetsBoot>falls back to"${{amount}}"(US dollars) — wrong for any non-USD store. See the token reference in@planetdataset/sdk-core.
Token scopes — your
PUBLIC_STOREFRONT_API_TOKENmust grant the unauthenticated Storefront API scopes the widgets use:
unauthenticated_read_product_listings— products & recommendationsunauthenticated_read_product_inventory— the recommendations widget filters onavailableForSale; without it products read as unavailable and nothing rendersunauthenticated_read_metaobjects— Smart Cart / cart-recommendations config (the metaobject definitions must also be exposed asPUBLIC_READ)unauthenticated_read_selling_plans— only if you use subscriptions
b) Mount <PlanetWidgetsBoot> (mandatory)
In the default App component, add the boot component. This step is required — without it, no Planet widget can render or talk to your cart.
import {PlanetWidgetsBoot} from '@planetdataset/sdk-hydrogen';
export default function App() {
const data = useRouteLoaderData('root');
return (
<Analytics.Provider cart={data.cart} shop={data.shop}>
<PageLayout {...data}>
<PlanetWidgetsBoot
storeDomain={data.planet.storeDomain}
publicAccessToken={data.planet.publicAccessToken}
shopId={data.planet.shopId}
countryCode={data.planet.countryCode}
languageCode={data.planet.languageCode}
moneyFormat={data.planet.moneyFormat}
/>
<Outlet />
</PageLayout>
</Analytics.Provider>
);
}<PlanetWidgetsBoot> uses the default planetCartApi automatically (POSTs to /cart, reads /cart.json) — no need to pass a cart prop unless you want to customize it.
c) Mount <PlanetSmartCartModal> (optional — only if you use the cart drawer)
If you plan to use the Smart Cart drawer (the slide-in panel that opens after add-to-cart and that usePlanetSmartCart() controls), add <PlanetSmartCartModal /> inside the <body> of the Layout component, and import the SDK styles:
import {PlanetWidgetsBoot, PlanetSmartCartModal} from '@planetdataset/sdk-hydrogen';
import planetSdkStyles from '@planetdataset/sdk-core/style.css?url';
export function Layout({children}) {
return (
<html>
<head>
<link rel="stylesheet" href={planetSdkStyles} />
{/* ... your existing head content ... */}
</head>
<body>
{children}
<PlanetSmartCartModal />
</body>
</html>
);
}Skip this section entirely if you only want product-page widgets (BOGO / Bundle / Upsell) and you'll handle the post-add-to-cart UX yourself (e.g. redirect to /cart).
3. Use widgets on product pages
import {PlanetBogo, PlanetBundle, PlanetUpsell} from '@planetdataset/sdk-hydrogen';
export default function Product() {
const {product} = useLoaderData();
const selectedVariant = useOptimisticVariant(product.selectedOrFirstAvailableVariant);
return (
<>
{/* Your own product UI */}
<PlanetBogo productId={product.id} variantId={selectedVariant.id} />
<PlanetBundle productId={product.id} variantId={selectedVariant.id} />
<PlanetUpsell productId={product.id} variantId={selectedVariant.id} />
</>
);
}Accepts both Shopify GIDs (gid://shopify/Product/123) and numeric strings — the wrappers normalize.
4. Open the cart from a button (header, hero CTA, etc.)
Optional — requires <PlanetSmartCartModal /> mounted (step 2c).
import {usePlanetSmartCart} from '@planetdataset/sdk-hydrogen';
function CartButton({count}) {
const {open, close, toggle} = usePlanetSmartCart();
return (
<button onClick={() => open()}>
Cart ({count})
</button>
);
}The drawer opens itself automatically after add-to-cart from a widget — you don't need to wire it up. Use the hook only for explicit user actions outside a widget context.
5. Show cart recommendations (the Smart Cart upsell, standalone)
<PlanetCartRecommendations> renders the same product-recommendation block as the Smart Cart drawer, but as a standalone widget you can place anywhere (cart page, custom drawer, etc.). Settings come from the merchant's Smart Cart config (fetched from the Storefront API) — no settings prop needed.
Requires the
/cart.jsonroute (Setup → step 1). The widget reads the cart (via the SDK cart API) to compute recommendations from the products in it — without that route it can't render. It's part of the mandatory<PlanetWidgetsBoot>setup, and the Smart Cart needs it too.
Pass the current Hydrogen cart (GraphQL shape — the resolved/optimistic cart from your loader). The wrapper pushes it into the widget on every render, so the recommendations stay reactive to every cart change (add, remove, quantity) with instant optimistic updates:
import {PlanetCartRecommendations} from '@planetdataset/sdk-hydrogen';
function CartRecommendations({cart}) {
return <PlanetCartRecommendations cart={cart} />;
}Why pass
cart? Without it, the widget's only sync path is the SDK's internal cart-change events — fired by the widget's own recommended-product add, adds routed throughPlanetWidgets.triggerAddToCart, or Planet smart-cart drawer ops. It does not react to mutations made directly through Hydrogen's native<CartForm>(remove a line, change quantity…), and<PlanetWidgetsBoot>'s revalidation listens to the same events (notCartForm). Passingcartis the reliable way to stay fully reactive.
Component reference
<PlanetWidgetsBoot>
Initializes the SDK on the client. Renders nothing. Mount once at the root, inside any context providers your cart adapter needs.
| Prop | Type | Default |
|---|---|---|
| storeDomain | string | required |
| publicAccessToken | string | required |
| shopId | string | required |
| countryCode | string | required |
| languageCode | string | required |
| moneyFormat | string | "${{amount}}" |
| advancedOptionsValues | object | sensible defaults derived from country code |
| cart | CartApi | planetCartApi (POST /cart, GET /cart.json) |
| revalidateCartOnChange | boolean | true |
moneyFormat is the Shopify money format string used to render all widget prices — fetch it from shop.moneyFormat in your root loader and pass it through (see Setup → step 2). It defaults to US dollars ("${{amount}}"), so non-USD stores must set it or prices show the wrong currency. Token reference: @planetdataset/sdk-core.
It also revalidates your loader cart data whenever a Planet widget mutates the cart (e.g. an add from <PlanetCartRecommendations>, which bypasses Hydrogen's CartForm), so the native cart drawer and badge refresh automatically — no PlanetCartSync component to write. Set revalidateCartOnChange={false} to opt out.
<PlanetSmartCartModal> / <PlanetSmartCart>
Drawer modal wrapper (mounts once at root) and drawer body (rarely used directly).
| Prop | Type |
|---|---|
| className | string |
| style | React.CSSProperties |
<PlanetBogo> / <PlanetBundle> / <PlanetUpsell>
Product-page widgets.
| Prop | Type | Notes |
|---|---|---|
| productId | string \| number | GID or numeric — auto-normalized |
| variantId | string \| number | GID or numeric — auto-normalized |
| className | string | |
| style | React.CSSProperties | |
<PlanetCartRecommendations>
The Smart Cart product-recommendation upsell as a standalone widget. Settings are fetched from the merchant's Smart Cart config (Storefront API), so there's no settings prop.
| Prop | Type | Notes |
|---|---|---|
| cart | object | Hydrogen GraphQL cart — converted via toLiquidCart and pushed in. Optional but recommended for host-driven sync |
| className | string | |
| style | React.CSSProperties | |
usePlanetSmartCart()
{
open: (refresh?: boolean) => Promise<void>;
close: () => Promise<void>;
toggle: () => Promise<void>;
}refresh: true re-fetches the cart before opening (useful after external mutations).
planetCartApi
Default cart adapter used by <PlanetWidgetsBoot>. Exports if you want to compose / wrap it:
import {planetCartApi} from '@planetdataset/sdk-hydrogen';
const customCart = {
...planetCartApi,
add: async (items) => {
const cart = await planetCartApi.add(items);
trackAnalytics(items);
return cart; // ← MUST return the cart
},
};Warning — if you wrap
add, you mustawaitandreturnthe promise. Otherwise the smart-cart drawer opens before/cart.jsonreflects the new line, and downstream consumers crash oncart().items.
toLiquidCart(hydrogenCart)
Hydrogen GraphQL cart → Liquid /cart.js shape. Used internally by the /cart.json route. Re-exported so you can use it in your own endpoints.
extractNumericId(gid)
gid://shopify/Foo/123 → "123". Returns empty string for falsy input.
Caveats
- Upsell
type: "checkbox"is Liquid-only — that mode injects variant IDs into a native Shopify<form>, which doesn't exist in Hydrogen. Usetype: "radio"ortype: "single"for headless storefronts. <planet-upsell-popup>isn't yet exposed via this package (next iteration).
Versioning
0.x.y — pre-1.0. APIs may change. Pin a minor version (^0.1.0 rather than latest).
