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. Merchant config is fetched once in a layout; each product page's WhimProvider fetches its own product on the server and passes it down 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.
Architecture
The SDK is organized around two scopes:
- Merchant scope:
getWhimConfig()fetches the merchant-level configuration once, typically in a layout.WhimConfigprovides it to client components via context. - Product scope:
<WhimProvider product="...">fetches one product's full data (variants, attributes, addons, prices) and manages the shopper's selection state. The product key is typically derived from the page's URL slug.
Both fetches run on the server during render: rental pricing is present in the initial HTML, and the client receives only the data the page uses. Hooks read from the nearest provider.
Setup
Install
npm install @whim-sdk/react
Requires react >=18. Ships ESM and CommonJS builds with full TypeScript declarations.
Merchant config
Fetch your merchant config in your product layout. This carries the merchant-level data (currency, checkout URL, program name, benefits, FAQs) and runs once for every page under the layout. Product data is not fetched here; each page's WhimProvider fetches its own.
// app/product/layout.tsx
import { getWhimConfig } from '@whim-sdk/react/server'
import { WhimConfig } from '@whim-sdk/react'
export default async function ProductLayout({ children }) {
const config = await getWhimConfig({ merchantId: 'your-merchant-id' })
return (
<WhimConfig config={config}>
{children}
</WhimConfig>
)
}
getWhimConfig() returns a WhimConfigData object. WhimConfig makes it available to all pages via context; useWhimBenefits(), useWhimFaqs(), and checkout URL building read from it.
fetch caching, and the Whim API responses are CDN-cached for several minutes. In Next.js, control how often server-rendered pricing refreshes with route segment options, e.g. export const revalidate = 600 for periodic revalidation or export const dynamic = 'force-dynamic' for per-request rendering.
| Option | Type | Description |
|---|---|---|
merchantId | string | Your Whim merchant ID |
apiBaseUrl | string | Optional. Whim API origin, defaults to production |
checkoutBaseUrl | string | Optional. Overrides the checkout base URL from the merchant config |
programName | string | Optional. Overrides the program verb (e.g. 'rent') |
benefits | string[] | Optional. Overrides the benefit bullets |
faqs | { question, answer }[] | Optional. Overrides the FAQ entries |
WhimProvider
Wrap the page where Whim hooks are used. An async server component: it fetches the named product on the server (so pricing is in the first-paint HTML), then holds the current mode, selected variant, and addon selections for its children. One provider per product.
<WhimProvider merchantId="your-merchant-id" product="your-product-key">
{/* useWhimProduct(), useWhimPrice(), etc. available here */}
</WhimProvider>
The product key resolves to either a configurable product (variants resolved by attributes) or a simple single-SKU product; hooks behave accordingly. The key is typically the page's URL slug. If your slugs differ from your Whim product keys, map them before rendering the provider.
| Prop | Type | Description |
|---|---|---|
merchantId | string | Your Whim merchant ID |
apiBaseUrl | string | Optional. Whim API origin, defaults to production |
product | string | Product key: a configurable product name or a simple product name |
initialAttributes | Record<string, string> | Optional. Starting variant attributes, defaults to the product's default variant |
syncModeToUrl | boolean | Optional, default true. Mirror buy/whim mode into the ?whim= URL param |
onNotFound | () => void | Optional. Called when the product key doesn't resolve; pass Next.js's notFound to 404 the page |
WhimProvider checks for ?whim=1 on mount. When present, it initializes in whim mode. setWhimMode() keeps the URL in sync via history.replaceState, no page reload. Disable with syncModeToUrl={false}.
Putting it together
Here's how the pieces fit across three files: product layout, page, and client component.
Product layout
Fetch the merchant config on the server with getWhimConfig() and wrap your pages in WhimConfig. Runs once and persists across navigations.
Product page
The URL slug is the product key. WhimProvider fetches the product on the server; pass notFound via onNotFound so unknown keys 404 the page.
Client component
Your existing product UI stays as-is, hooks are available to any component inside the provider.
// app/product/layout.tsx (server)
import { getWhimConfig } from '@whim-sdk/react/server'
import { WhimConfig } from '@whim-sdk/react'
export default async function ProductLayout({ children }) {
const config = await getWhimConfig({ merchantId: 'your-merchant-id' })
return (
<WhimConfig config={config}>
{children}
</WhimConfig>
)
}
// app/product/[slug]/page.tsx (server)
import { WhimProvider } from '@whim-sdk/react'
import { notFound } from 'next/navigation'
import { ProductInfo } from './product-info'
export default async function Page({ params }) {
const { slug } = await params
return (
<WhimProvider
merchantId="your-merchant-id"
product={slug}
onNotFound={notFound}
>
<ProductInfo />
</WhimProvider>
)
}
// app/product/[slug]/product-info.tsx
'use client'
import { useWhimMode, useWhimPrice } from '@whim-sdk/react'
export function ProductInfo() {
const { mode, setWhimMode } = useWhimMode()
const price = useWhimPrice()
// your existing PDP UI, wired to the hooks below
}
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>
)
}
Client-side rendering
Apps without React Server Components (SPAs, or any fully client-rendered tree) import the provider from @whim-sdk/react/client instead. Same props, same hooks, same children. The only difference is that the product data is fetched in the browser after mount, so hooks return null and useWhimProduct().isLoading is true until it arrives.
import { WhimProvider } from '@whim-sdk/react/client'
<WhimProvider merchantId="your-merchant-id" product="your-product-key">
<ProductInfo />
</WhimProvider>
function ProductInfo() {
const { product, isLoading } = useWhimProduct()
if (isLoading) return <PriceSkeleton />
// ...
}
The default provider from @whim-sdk/react is an async server component, which React does not support inside a client-rendered tree; the /client entry point exists for that case.
Multiple products
Mount one WhimProvider per product. Each provider holds its own mode, variant, and addon state, and hooks always refer to the product of the nearest provider. Server providers rendered as siblings fetch in parallel.
{['camera-a', 'camera-b', 'mic-c'].map((key) => (
<WhimProvider
key={key}
merchantId="your-merchant-id"
product={key}
syncModeToUrl={false}
>
<RentableCard /> {/* uses hooks, needs no product props */}
</WhimProvider>
))}
Set syncModeToUrl={false} on all but the primary product so only one provider writes to the ?whim= URL param. A kit page with selectable extras is usually better modeled as one configurable product with addons than as multiple providers; contact Whim to review your catalog setup. Multi-product checkout (a cart) is on the roadmap.
Unknown products
When the product key doesn't resolve to a catalog product, the provider reports it and lets your page decide the policy. There are two ways to handle it.
Hard 404 for dead URLs: pass Next.js's notFound via onNotFound. This is not a redirect. Next renders the nearest not-found.tsx boundary in place of the page content, inside the surrounding layouts, with the URL unchanged and an HTTP 404 status; see the Next.js documentation for exact status code behavior. Place the boundary file at the segment whose layout should wrap the message.
// app/product/[slug]/page.tsx
import { notFound } from 'next/navigation'
<WhimProvider merchantId="your-merchant-id" product={slug} onNotFound={notFound}>
<ProductInfo />
</WhimProvider>
// app/product/not-found.tsx renders inside app/product/layout.tsx,
// so your header and page chrome stay on screen around the 404.
Soft, in-place handling for a miss inside an otherwise-valid page: omit onNotFound. The provider renders its children anyway with null product data, and useWhimProduct() reports reason: WhimUnavailableReason.NotFound, so any component can branch and render its own unavailable state.
import { useWhimProduct, WhimUnavailableReason } from '@whim-sdk/react'
function ProductInfo() {
const { product, reason } = useWhimProduct()
if (reason === WhimUnavailableReason.NotFound) {
return <p>This product isn't available to rent right now.</p>
}
// ...
}
onNotFound={notFound} for dead URLs so crawlers and monitoring see a 404. The callback is generic, so redirect policies also work: onNotFound={() => redirect('/products')}.
Reference
All hooks read from the nearest WhimProvider. They throw if called outside one. useWhimBenefits() and useWhimFaqs() read from WhimConfig.
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
configurableProduct, // full WhimConfigurableProduct (variants, attributes), or null
isAvailable, // false when no variant matches the Whim catalog
reason, // WhimUnavailableReason enum member, or null
isLoading, // true while the client-side provider is still fetching
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={!isAvailable}>
{isAvailable ? 'Rent Monthly' : 'Rent unavailable'}
</button>
Returns { product, configurableProduct, isAvailable, reason, isLoading, setWhimAttribute, attributeDefs }
When setWhimAttribute() resolves a different variant, addon selections reset to that variant's defaults: required addons are re-applied, optional ones are cleared. Different variants can have different available addons, so resetting is the safe default.
Resolution reasons (the WhimUnavailableReason enum) when isAvailable is false:
NotFound- the key passed toWhimProviderdoesn't exist in the catalog (see Unknown products for both handling patterns)IncompleteAttributes- not enough attributes selected to resolve a variantNoVariantMatch- attributes selected but no variant in the catalog matches themOutOfStock- 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 the merchant config's checkoutBaseUrl, so it must be used inside both WhimConfig and WhimProvider.
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 isAvailable 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.- When
setWhimAttribute()resolves a different variant, addon selections reset to that variant's defaults: required addons are re-applied, optional selections are cleared.
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 }[]
Types
WhimConfigData
Returned by getWhimConfig(), passed to WhimConfig.
interface WhimConfigData {
merchantId: string
apiBaseUrl: string
currency: string
checkoutBaseUrl: string
programName: string // 'rent', 'subscribe', 'try', etc.
benefits: string[] // marketing copy
faqs: { question: string, answer: string }[]
}
WhimProductData
Returned by getWhimProduct(), or null when the key doesn't exist (HTTP 404). Network and server errors throw instead, so outages surface as errors rather than 404s. This is what WhimProvider fetches internally.
type WhimProductData =
| { type: 'configurable', configurable: WhimConfigurableProduct }
| { type: 'simple', product: WhimProduct }
WhimConfigurableProduct
interface WhimConfigurableProduct {
name: string // e.g. 'your-product-name'
displayName: string
attributes: WhimConfigurableAttribute[]
products: WhimProduct[] // rentable variants under this configurable
defaultProduct?: WhimProduct
addons: WhimAddonDef[]
}
interface WhimConfigurableAttribute {
name: string // 'size', 'color'
selectorType?: string // UI hint, e.g. 'color'
options: {
name: string // display label: 'Midnight Black'
value: string // attribute value: 'black'
urlValue: string // checkout URL form: 'mb'
}[]
}
WhimAttributeDef
interface WhimAttributeDef {
name: string // 'size', 'color'
values: string[] // ['small', 'large'], ['black', 'white']
}
WhimProduct
interface WhimProduct {
name: string // 'your-product-large-black'
displayName: string
images: string[]
isOutOfStock: boolean
attributeValues?: WhimAttributeValue[]
addonKeys?: 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 {
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: { name: string, displayName?: string, price: WhimProductPrice }
}[]
}
WhimUnavailableReason
Enum returned as reason by useWhimProduct() when isAvailable is false. Exported from @whim-sdk/react as a value, so compare against its members.
enum WhimUnavailableReason {
NotFound = 'NOT_FOUND',
IncompleteAttributes = 'INCOMPLETE_ATTRIBUTES',
NoVariantMatch = 'NO_VARIANT_MATCH',
OutOfStock = 'OUT_OF_STOCK',
}