React / Next.js
Providers and hooks for React and Next.js storefronts. Works with the App Router so rental prices are in the HTML on first paint.
The script SDK (window.whim) works for Shopify, static HTML, and any page where you can drop in a <script> tag. This package is for merchants who server-render with React.
About
Overview
@whim-sdk/react gives you providers and hooks for Whim pricing in React. Product data is fetched on the server and passed through context, so prices are in the HTML before the page hydrates.
When to use
Use @whim-sdk/react if your storefront is a Next.js App Router project or similar React SSR framework. If you're on Shopify, WordPress, or plain HTML, use the script SDK instead.
Setup
Install
The @whim-sdk/react package will be provided upon kickoff. Requires react >=18.
Server Setup
Fetch the Whim catalog in your product layout. This is a server-only import, it never ships to the client bundle.
// app/product/layout.tsx
import { getWhimState } from '@whim-sdk/react/server'
import { WhimConfig } from '@whim-sdk/react'
export default async function ProductLayout({ children }) {
const whimState = await getWhimState({ merchantId: 'your-merchant-id' })
return (
<WhimConfig state={whimState}>
{children}
</WhimConfig>
)
}
getWhimState() returns a WhimState object containing the full product catalog, pricing, and merchant config. WhimConfig makes this available to all pages via context.
| Option | Type | Description |
|---|---|---|
merchantId | string | Your Whim merchant ID |
products | string[] | Optional. Filter catalog to specific product names |
WhimProvider
Wrap the page where Whim hooks are used. Holds the current mode, selected variant, and addon selections. Reads the catalog from WhimConfig automatically.
Simple product (single SKU, no variants):
<WhimProvider product="your-product-name">
{/* useWhimMode(), useWhimPrice(), etc. available here */}
</WhimProvider>
Configurable product (variants resolved by attributes):
// initialAttributes syncs the provider with whatever variant
// the page loaded with, falls back to catalog default if omitted
<WhimProvider
configurableProduct="your-product-name"
initialAttributes={{ size: 'large', color: 'black' }}
>
{/* useWhimProduct(), useWhimAddons(), etc. available here */}
</WhimProvider>
| Prop | Type | Description |
|---|---|---|
product | string | Whim product name for simple (single-SKU) products |
configurableProduct | string | Whim configurable product name for variant products |
initialAttributes | Record<string, string> | Starting variant attributes. Only used with configurableProduct |
WhimProvider checks for ?whim=1 on mount via useSearchParams(). When present, it initializes in whim mode. setWhimMode() keeps the URL in sync via history.replaceState, no page reload.
Putting it together
Here's how the pieces fit across three files: product layout, page, and client component.
Product layout
Fetch the Whim catalog on the server with getWhimState() and wrap your pages in WhimConfig. Runs once and persists across navigations.
Product page
Load your own product data and pass it to the client component. No Whim logic here, this is your existing page.
Client component
Wrap with WhimProvider. Your existing product UI stays as-is, hooks are available to any component inside.
// app/product/layout.tsx (server)
import { getWhimState } from '@whim-sdk/react/server'
import { WhimConfig } from '@whim-sdk/react'
export default async function ProductLayout({ children }) {
const whimState = await getWhimState({ merchantId: 'your-merchant-id' })
return (
<WhimConfig state={whimState}>
{children}
</WhimConfig>
)
}
// app/product/[slug]/page.tsx (server)
import { Product } from './Product'
export default async function Page({ params }) {
const product = await getProduct(params.slug)
return <Product product={product} />
}
// app/product/[slug]/Product.tsx
'use client'
import { WhimProvider } from '@whim-sdk/react'
import { ProductInfo } from './product-info'
export function Product({ product }) {
return (
<WhimProvider
configurableProduct={product.whimName}
initialAttributes={{ color: '...', style: '...' }}
>
<ProductInfo product={product} />
</WhimProvider>
)
}
PDP integration
Inside the provider, use hooks to read Whim state and wire it into your existing product UI.
Mode toggle
useWhimMode() gives you the current mode and a setter. Build your own toggle UI around it.
Price display
useWhimPrice() returns the rental price. Show your buy price in buy mode, the Whim price in whim mode.
Checkout
useWhimCheckoutUrl() builds the checkout URL. Point your CTA at it in whim mode, your own cart in buy mode.
import { useWhimMode, useWhimPrice, useWhimCheckoutUrl } from '@whim-sdk/react'
function ProductInfo({ product }) {
const { mode, setWhimMode, programName } = useWhimMode()
const isWhim = mode === 'whim'
const whimPrice = useWhimPrice()
const buyPrice = product.currentVariant?.price
const whimCheckoutUrl = useWhimCheckoutUrl()
const buyCheckoutUrl = product.currentVariant?.cartUrl
return (
<div>
<div className="mode-toggle">
<button onClick={() => setWhimMode('buy')}>
Buy — {buyPrice}
</button>
<button onClick={() => setWhimMode('whim')}>
{programName} — {fmt(whimPrice?.recurring?.[0]?.amount)}
</button>
</div>
<p className="price">
{isWhim ? fmt(whimPrice?.recurring?.[0]?.amount) : buyPrice}
</p>
<a href={isWhim ? whimCheckoutUrl : buyCheckoutUrl}>
{isWhim ? `${programName} Now` : 'Add to Cart'}
</a>
</div>
)
}
Reference
All hooks read from the nearest WhimProvider. They throw if called outside one.
useWhimMode()
Current mode and a setter to switch between buy and whim.
const { mode, setWhimMode, programName } = useWhimMode()
// mode: 'buy' | 'whim'
// programName: merchant-configured label, e.g. 'Rent', 'Subscribe', 'Try'
<button onClick={() => setWhimMode('buy')}>Buy</button>
<button onClick={() => setWhimMode('whim')}>{programName}</button>
Returns { mode, setWhimMode, programName }
setWhimMode('whim') adds ?whim=1 to the URL. setWhimMode('buy') removes it. No page reload.
useWhimProduct()
The currently resolved product and attribute controls. For configurable products, call setWhimAttribute() when the user picks a variant and the provider resolves the matching product automatically.
const {
product, // resolved WhimProduct, or null
isRentable, // false when no variant matches the Whim catalog
reason, // 'NO_RENTABLE_MATCH' | 'OUT_OF_STOCK' | ... | null
setWhimAttribute, // ('size', 'large') - resolves new variant
attributeDefs, // [{ name: 'size', values: ['small', 'large'] }, ...]
} = useWhimProduct()
// wire your existing pickers, one extra line each
function handleSizeChange(newSize) {
setSize(newSize) // your own state
setWhimAttribute('size', newSize) // sync with Whim
}
// disable rent when variant isn't rentable
<button disabled={!isRentable}>
{isRentable ? 'Rent Monthly' : 'Rent unavailable'}
</button>
Returns { product, isRentable, reason, setWhimAttribute, attributeDefs }
setWhimAttribute() clears selected addons (v1). Different variants can have different available addons, so clearing is the safe default.
Resolution reasons when isRentable is false:
UNKNOWN_PRODUCT- the product name doesn't exist in the catalogINCOMPLETE_ATTRIBUTES- not enough attributes selected to resolve a variantNO_RENTABLE_MATCH- attributes selected but no matching rentable variantOUT_OF_STOCK- variant exists but is currently unavailable
useWhimPrice()
Pricing for the current product plus selected addons. Recurring and one-time costs are split so you can render them however you want.
const price = useWhimPrice()
// amounts are in cents — format however you want
{price && (
<>
{price.recurring.map((r) => (
<p key={r.interval}>
{fmt(r.amount)}/{r.interval.toLowerCase()}
</p>
))}
{price.oneTime && (
<p>{fmt(price.oneTime.amount)} today</p>
)}
{price.introDiscount && (
<p>
{fmt(price.introDiscount.amount)} for first
{' '}{price.introDiscount.months} months
</p>
)}
</>
)}
Returns WhimResolvedPrice | null
Returns null before a product is resolved. See WhimResolvedPrice for the full shape.
useWhimCheckoutUrl()
Checkout URL for the current product, variant attributes, and selected addons. Built client-side from WhimState.checkoutBaseUrl.
const whimCheckoutUrl = useWhimCheckoutUrl()
// e.g. https://rent.example.com/merchant/checkout?products=your-product-name(size=large,color=black,addons(case=c1))
<a href={isWhim ? whimCheckoutUrl : product.cartUrl}>
{isWhim ? `${programName} Now` : 'Add to Cart'}
</a>
Returns string | null
Returns null when no product is resolved or isRentable is false.
useWhimAddons()
Addon definitions for the current variant and selection state. The provider filters to addons available for the current variant automatically.
const { addonDefs, addons, setWhimAddon, removeWhimAddon } = useWhimAddons()
// addonDefs - addon definitions for the current variant
// addons - current selections: { case: 'c1', warranty: 'w1' }
// single-option addon (warranty) - checkbox
{addonDefs.filter(a => a.isOptional).map((addon) =>
addon.options.length === 1 ? (
<Checkbox
checked={!!addons[addon.addonKey]}
onToggle={(on) => on
? setWhimAddon(addon.addonKey, addon.options[0].optionKey)
: removeWhimAddon(addon.addonKey)
}
/>
) : (
// multi-option addon (case with choices) - dropdown
<Select
value={addons[addon.addonKey]}
onChange={(val) => val
? setWhimAddon(addon.addonKey, val)
: removeWhimAddon(addon.addonKey)
}
/>
)
)}
Returns { addonDefs, addons, setWhimAddon, removeWhimAddon }
Key addon behaviors:
isOptional: true- user opts in (render as checkbox or dropdown)isOptional: false- required, auto-included usingdefaultOptionKey. Don't render as toggleable UI.type: 'FEE'vs'PRODUCT'- fees are charges (insurance, shipping), products are physical addons. Same shape, different checkout display.- Addon selections clear when
setWhimAttribute()changes the variant (v1).
useWhimBenefits()
Marketing copy from the merchant config. Useful for showing rental benefits alongside the price.
const benefits = useWhimBenefits()
// ['Free shipping', 'Cancel anytime', '30-day trial']
{benefits.length > 0 && (
<ul>{benefits.map((b) => <li key={b}>{b}</li>)}</ul>
)}
Returns string[]
useWhimFaqs()
FAQ entries from the merchant config. Useful for rendering a rental FAQ section on the PDP.
const faqs = useWhimFaqs()
// [{ question: 'How does renting work?', answer: 'Pick a plan...' }, ...]
{faqs.length > 0 && (
<dl>
{faqs.map((faq) => (
<div key={faq.question}>
<dt>{faq.question}</dt>
<dd>{faq.answer}</dd>
</div>
))}
</dl>
)}
Returns { question: string, answer: string }[]
Components
<WhimTeaserPrice>
Teaser for listing pages and product grids. Renders a one-liner like "or rent for $29/mo" next to the buy price. The program name (rent, try, subscribe) comes from your merchant config.
<p className="buy-price">$249.00</p>
<WhimTeaserPrice product="wireless-headphones-black" />
{/* renders: "or rent for $29/mo" */}
Props
| Prop | Required | Description |
|---|---|---|
product | Yes | The Whim product name (or your mapped ID) |
className | No | CSS class for the rendered <span> |
Renders as an inline <span>. Shows the intro discount price when available. Renders nothing if the product is not found.
Types
WhimState
Returned by getWhimState(). Contains the full merchant catalog, passed to WhimConfig.
interface WhimState {
merchantId: string
currency: string
checkoutBaseUrl: string
programName: string // 'Rent', 'Subscribe', 'Try', etc.
benefits: string[] // marketing copy
faqs: { question: string, answer: string }[]
products: WhimProduct[] // simple products (single SKU)
configurableProducts: WhimConfigurableProduct[]
}
WhimConfigurableProduct
interface WhimConfigurableProduct {
name: string // e.g. 'your-product-name'
displayName: string
attributes: WhimAttributeDef[]
products: WhimProduct[] // rentable variants under this configurable
defaultProduct?: WhimProduct
addons: WhimAddonDef[]
}
WhimAttributeDef
interface WhimAttributeDef {
name: string // 'size', 'color'
values: string[] // ['small', 'large'], ['black', 'white']
}
WhimProduct
interface WhimProduct {
id: string
name: string // 'your-product-large-black'
displayName: string
images: string[]
attributeValues?: WhimAttributeValue[]
addonIds: string[] // which addon defs apply to this variant
price: WhimProductPrice
discountPercent: number | null
discountMonths: number | null
}
interface WhimAttributeValue {
name: string // attribute name: 'size'
value: string // selected value: 'large'
}
interface WhimProductPrice {
amount: number // cents, e.g. 4900
compareAtAmount: number | null // buy price reference, e.g. 59900
isRecurring: boolean
recurringInterval: string | null // 'MONTH', 'YEAR', etc.
recurringCount: number | null
}
WhimResolvedPrice
Returned by useWhimPrice(). Base product price plus selected addons, grouped by billing cadence. Amounts are in cents — formatting is up to you.
interface WhimResolvedPrice {
recurring: {
amount: number // cents, e.g. 4900
interval: string // 'MONTH', 'YEAR', etc.
count: number // e.g. 1
}[] // grouped by cadence — usually one entry
oneTime: {
amount: number // cents
} | null
introDiscount: {
amount: number // discounted cents
months: number
} | null
}
WhimAddonDef
interface WhimAddonDef {
id: string
name: string
displayName: string
addonKey: string // checkout URL key: 'case', 'warranty'
isOptional: boolean
defaultOptionKey: string | null
type: 'FEE' | 'PRODUCT'
options: {
optionKey: string // checkout URL value: 'c1', 'w1'
product: WhimProduct
}[]
}