React
Wire ScaleBun into a React app — an error boundary for render errors, a router-agnostic route hook, reactive feature flags, coachmark anchors, and in-app message slots.
Install#
npm i @scalebun/web @scalebun/web-reactPeer dependency: react >=17.
Initialize#
One component does the whole integration: it initializes the SDK once (client-side only), installs the render-error boundary, and provides the in-app messaging client.
import { ScaleBunProvider } from '@scalebun/web-react';
<ScaleBunProvider config={{ clientKey: 'skb_live_ck_…', apiBaseUrl: 'https://api.scalebun.com/api/v1' }} routeName={location.pathname} onReady={() => ScaleBun.identify(currentUserId)}> <App /></ScaleBunProvider>;| Prop | Default | What it does |
|---|---|---|
config | — | Required. Passed to ScaleBun.init. |
routeName | — | Current route, reported via trackScreen. |
errorBoundary | true | false keeps your own error handling. |
fallback / onError | — | Forwarded to the boundary. |
inApp | true | false if you supply your own <InAppProvider>. |
inAppClient / inAppTheme | — | Bring your own client, or set the theme. |
onReady | — | Fires once after init resolves — the right place for identify(). |
Composing it yourself#
The provider is composition over parts that are all still exported, so the granular form keeps working if you want the pieces separately:
import ScaleBun, { ScaleBunErrorBoundary, useRouteName } from '@scalebun/web-react';
void ScaleBun.init({ clientKey, apiBaseUrl }); // module scopeRender errors#
React swallows render errors before window.onerror sees them, so the boundary
is the only way to capture them.
import { ScaleBunErrorBoundary } from '@scalebun/web-react';
<ScaleBunErrorBoundary fallback={(error) => <p>Something broke: {error.message}</p>} onError={(error, info) => console.warn(info.componentStack)}> <App /></ScaleBunErrorBoundary>;| Prop | Type | Notes |
|---|---|---|
children | ReactNode | Required. |
fallback | ReactNode | (error) => ReactNode | Rendered after a caught error. Omitted renders nothing. |
onError | (error, info) => void | Runs after the error is captured. |
The React componentStack is attached to the captured error automatically.
Route names#
useRouteName is router-agnostic — pass whatever your router exposes. It fires
once per distinct value.
import { useLocation } from 'react-router-dom';import { useRouteName } from '@scalebun/web-react';
function App() { useRouteName(useLocation().pathname); return <Routes />;}Prefer a matched pattern over a raw pathname where your router offers one:
/orders/:id groups in the dashboard, /orders/8412 does not.
Feature flags#
useFlag subscribes the component to the flag and re-renders when the value
changes — so a kill-switch flipped in the dashboard reaches the screen, not just
the cache.
import { useFlag } from '@scalebun/web-react';
function Checkout() { const variant = useFlag('checkout-cta', 'control'); return variant === 'treatment' ? <NewCta /> : <Cta />;}The fallback is required and is what renders before the first config payload arrives, during SSR, and if the key is missing — so pass the value you would ship without the flag.
Reading the facade#
import { useScaleBun } from '@scalebun/web-react';
const scalebun = useScaleBun();scalebun.track('cart_viewed', { items: 3 });Coachmarks#
Mark elements a tour can anchor to. data-scalebun-anchor is the most
churn-resistant anchor strategy — it survives class and DOM changes that break a
CSS selector.
import { useCoachmarks, ScaleBunAnchor, useScaleBunAnchor } from '@scalebun/web-react';
<ScaleBunAnchor name="checkout"> <button>Checkout</button></ScaleBunAnchor>;
// or spread the attribute onto your own element<button {...useScaleBunAnchor('checkout')}>Checkout</button>;
const coach = useCoachmarks();coach.startById('onboarding');In-app messages#
Inline formats render in the page, so they need a slot to mount into.
import { InAppProvider, InAppSlot, useInApp } from '@scalebun/web-react';
<InAppProvider theme="auto"> <InAppSlot name="empty-state" /></InAppProvider>;<InAppProvider> creates and owns one client; pass client to supply your own.
For a custom element, useInAppSlot(name) returns a ref to attach. useInApp()
throws outside a provider — that is deliberate, so a silently dead slot is not
possible.
Waiting for the SDK#
Component code that reaches for a feature controller should await
whenReady() — it resolves once init() has settled, succeeded or failed.
useEffect(() => { let live = true; void ScaleBun.whenReady().then(() => { if (live) ScaleBun.inbox().mount({ trigger: bellRef.current! }); }); return () => { live = false; };}, []);(The inbox is one of the opt-in features — enable it with
features: { inbox: true } at init, or the controller is a no-op for a second
reason.)
Without it, an accessor called before the runtime exists returns a no-op object — every method silently does nothing, forever. In a component tree that is the normal case rather than an edge case.
Next#
Next.js — if you are on the App Router.
Flags, config & experiments — targeting and rollouts.
Errors & crashes — what gets captured.