@kitenzo/react
v0.2.6
Published
React hooks and provider for [Kitenzo Bundle Builder](https://apps.shopify.com/bundlebuilder). Provides everything you need to build custom bundle experiences in React and Shopify Hydrogen storefronts — you bring the UI, we handle the data.
Readme
@kitenzo/react
React hooks and provider for Kitenzo Bundle Builder. Provides everything you need to build custom bundle experiences in React and Shopify Hydrogen storefronts — you bring the UI, we handle the data.
Built on @kitenzo/core and re-exports everything from it, so you only need a single install.
Install
npm install @kitenzo/reactPeer dependencies: react >= 18, react-dom >= 18. Optional: @shopify/hydrogen-react >= 2024.0.0.
Quick Start
1. Add the Provider
import { KitenzoProvider } from '@kitenzo/react';
function App() {
return (
<KitenzoProvider apiKey={import.meta.env.PUBLIC_KITENZO_API_KEY}>
<Outlet />
</KitenzoProvider>
);
}| Prop | Type | Required | Description |
|------|------|----------|-------------|
| apiKey | string | Yes | Kitenzo API key (kit_live_... or kit_test_...) |
| baseUrl | string | No | Override the default API base URL |
| apiVersion | string | No | API version (default: v1) |
The provider automatically fetches shop settings (currency, moneyFormat) on mount.
2. Build a Bundle Page
import {
useBundle,
useBundleBuilder,
useBundlePrice,
useBundleCart,
buildCartPayload,
} from '@kitenzo/react';
function BundlePage({ bundleId }: { bundleId: number }) {
const { bundle, isLoading, error } = useBundle(bundleId);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!bundle) return null;
return <BundleConfigurator bundle={bundle} />;
}
function BundleConfigurator({ bundle }: { bundle: BundleDetail }) {
const {
selections, currentSection, currentSectionIndex,
addItem, removeItem, nextSection, prevSection, goToSection,
isSectionValid, isComplete, errors,
} = useBundleBuilder(bundle);
const { formattedOriginalPrice, formattedDiscountedPrice, hasDiscount } =
useBundlePrice(bundle, selections);
const { addBundleToCart, isLoading: isSubmitting } = useBundleCart();
async function handleSubmit() {
const result = await addBundleToCart(bundle, selections);
const { lines, attributes } = buildCartPayload(result);
// Add to your cart (Hydrogen, custom Storefront API, etc.)
}
return (
<div>
<h1>{bundle.name}</h1>
{/* Section tabs */}
{bundle.sections.map((section, i) => (
<button key={section.id} onClick={() => goToSection(i)}>
{section.name}
</button>
))}
{/* Products in current section */}
{currentSection?.products.map((product) => (
<div key={product.id}>
<h3>{product.title}</h3>
{product.variants.map((variant) => {
const selected = (selections[currentSection.id] ?? [])
.some((s) => s.variantId === variant.id);
return (
<button
key={variant.id}
disabled={!variant.available}
onClick={() =>
selected
? removeItem(currentSection.id, variant.id)
: addItem(currentSection.id, variant.id)
}
>
{selected ? '✓ ' : ''}{variant.title} - {variant.price}
</button>
);
})}
</div>
))}
{/* Price */}
{hasDiscount ? (
<p><s>{formattedOriginalPrice}</s> {formattedDiscountedPrice}</p>
) : (
<p>{formattedDiscountedPrice}</p>
)}
{/* Navigation */}
<button onClick={prevSection}>Back</button>
<button onClick={nextSection} disabled={!isSectionValid}>Next</button>
{isComplete && (
<button onClick={handleSubmit} disabled={isSubmitting}>
Add to Cart
</button>
)}
</div>
);
}Full Bundle Embed
If you don't need a custom UI, use <BundleEmbed> to render the full admin-configured bundle experience — the same UI merchants see on their storefront.
import { BundleEmbed } from '@kitenzo/react';
function BundlePage({ bundleId }: { bundleId: number }) {
return (
<BundleEmbed
bundleId={bundleId}
apiKey="kit_live_..."
shopDomain="my-store.myshopify.com"
onAddToCart={({ lines }) => {
cart.linesAdd(lines);
}}
/>
);
}| Prop | Type | Required | Description |
|------|------|----------|-------------|
| bundleId | number | Yes | Bundle to display |
| apiKey | string | Yes | Kitenzo API key |
| shopDomain | string | Yes | Shop's myshopify.com domain |
| onAddToCart | (payload: EmbedCartPayload) => void | Yes | Called with normalized Storefront API cart lines |
| onError | (error: Error) => void | No | Called on script load or settings fetch failure |
| baseUrl | string | No | Override the default API base URL |
| className | string | No | CSS class for the container |
Note:
<BundleEmbed>is self-contained — it does not require<KitenzoProvider>. Only one embed per page is supported.
Hooks
useBundles()
Fetches the list of published bundles.
const { bundles, isLoading, error, refetch } = useBundles();useBundle(bundleId, options?)
Fetches a single bundle with full product and variant data.
const { bundle, isLoading, error, refetch } = useBundle(bundleId);
// With SSR hydration:
const { bundle } = useBundle(bundleId, { initialData: loaderBundle });useBundleBuilder(bundle)
State machine for step-by-step bundle configuration. Wraps createBundleBuilder from core with useSyncExternalStore.
const {
// State
selections, // Record<sectionId, BundleSelection[]>
currentSection, // BundleSection | null
currentSectionIndex, // number
isSectionValid, // current section meets min/max
isValid, // all sections meet min/max
isComplete, // all valid + rules pass + required products present
allItems, // flat list of all selections
errors, // ValidationError[] — limit rule / required product violations
// Mutations
addItem, // (sectionId, variantId, quantity?) => void
removeItem, // (sectionId, variantId) => void
updateQuantity, // (sectionId, variantId, quantity) => void
reset, // () => void
// Navigation
nextSection, // () => void
prevSection, // () => void
goToSection, // (index) => void
// Queries
getSectionQuantity, // (sectionId) => number
} = useBundleBuilder(bundle);useBundleCart()
Submits a bundle configuration to the API and returns cart-ready data.
const { addBundleToCart, isLoading, error, lastResult } = useBundleCart();
const result = await addBundleToCart(bundle, selections, { countryCode: 'US' });
// result.variantId, result.configuredBundleId, result.pricing, etc.useBundlePrice(bundle, selections, options?)
Calculates pricing locally — no API call, no loading state. Updates instantly as selections change. Currency and money format are read automatically from shop settings. Pass { currency } to override.
const {
formattedOriginalPrice, // "$45.00" (formatted with shop's moneyFormat)
formattedDiscountedPrice, // "$40.50"
hasDiscount, // true
originalPrice, // "45.00" (raw)
discountedPrice, // "40.50" (raw)
discountType, // "percentage"
discountValue, // "10.00"
currency, // "USD"
} = useBundlePrice(bundle, selections);useSettings()
Returns shop settings auto-fetched by KitenzoProvider. Returns null while loading.
const settings = useSettings();
// settings?.currency — "USD", "EUR", etc.
// settings?.moneyFormat — "${{amount}}", "€{{amount}}", etc.useKitenzo()
Returns the KitenzoClient instance from the nearest <KitenzoProvider>. Useful for direct API calls.
const client = useKitenzo();
const settings = await client.getSettings();Core Re-exports
Everything from @kitenzo/core is re-exported, so React consumers only need @kitenzo/react:
import {
KitenzoClient,
createBundleBuilder,
buildCartPayload,
buildCartLines,
calculatePrice,
formatMoney,
} from '@kitenzo/react';See the @kitenzo/core README for client, builder, and utility documentation.
SSR with Remix / Hydrogen
import { KitenzoClient, useBundle, useBundleBuilder } from '@kitenzo/react';
export async function loader({ params, context }) {
const client = new KitenzoClient({
apiKey: context.env.KITENZO_API_KEY,
});
return { bundle: await client.getBundle(Number(params.id)) };
}
export default function BundlePage() {
const { bundle: loaderBundle } = useLoaderData();
const { bundle } = useBundle(loaderBundle.id, { initialData: loaderBundle });
// ... build your UI with useBundleBuilder, useBundlePrice, etc.
}Troubleshooting
"Missing KitenzoProvider" -- Ensure <KitenzoProvider> wraps the component tree.
CORS errors -- Check Allowed origins in Settings > Headless.
No discount at checkout -- Make sure cart attributes from buildCartPayload() are applied via cartAttributesUpdate().
Price shows null -- Pricing only calculates once at least one variant is selected.
