@whim-sdk/react

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.

OptionTypeDescription
merchantIdstringYour Whim merchant ID
productsstring[]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>
PropTypeDescription
productstringWhim product name for simple (single-SKU) products
configurableProductstringWhim configurable product name for variant products
initialAttributesRecord<string, string>Starting variant attributes. Only used with configurableProduct
URL-based mode: 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.

1

Product layout

Fetch the Whim catalog on the server with getWhimState() and wrap your pages in WhimConfig. Runs once and persists across navigations.

2

Product page

Load your own product data and pass it to the client component. No Whim logic here, this is your existing page.

3

Client component

Wrap with WhimProvider. Your existing product UI stays as-is, hooks are available to any component inside.

Code
// 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.

Preview
Product Name
Large / Black
$599
1

Mode toggle

useWhimMode() gives you the current mode and a setter. Build your own toggle UI around it.

2

Price display

useWhimPrice() returns the rental price. Show your buy price in buy mode, the Whim price in whim mode.

3

Checkout

useWhimCheckoutUrl() builds the checkout URL. Point your CTA at it in whim mode, your own cart in buy mode.

Code
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:


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:


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

PropRequiredDescription
productYesThe Whim product name (or your mapped ID)
classNameNoCSS 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
  }[]
}