React
SplitHubProvider with a property, <Experiment>/<Variant>, useVariant, useValue, useTrack, useIdentify, automatic pageviews.
@splithub/react collects analytics and renders experiments in any React 18+ app (Vite, CRA, Remix, Astro islands). Next.js App Router has its own package, see Next.js; so do React Native and Expo, see React Native and Expo.
npm i @splithub/react @splithub/core
Provider
import { SplitHubProvider } from '@splithub/react';
import config from '../splithub.config';
createRoot(document.getElementById('root')!).render(
<SplitHubProvider config={config} property="sh-x1h78w">
<App />
</SplitHubProvider>
);
The provider creates the client synchronously, reads or sets the sh_vid cookie, applies cached live overrides (cloud mode) and starts the event queue. With property it also records pageviews and events for that analytics property. Props: config, property?, visitorId? (SSR: pass the id from the cookie so server and client agree), overrides?, client?, pageviews?, trackNavigation?, context?, remote?, autoGoals?, devtools?.
Component experiments
import { Experiment, Variant } from '@splithub/react';
<Experiment id="hero-cta">
<Variant name="control"><Hero /></Variant>
<Variant name="bold"><HeroBold /></Variant>
</Experiment>
One <Variant> renders by assignment; the exposure is recorded on mount. Unknown or not-running experiments render the control.
Value experiments
const title = useValue<string>('hero-title');
const price = useValue<number>('price-anchor');
const layout = useValue<{ columns: number; dense: boolean }>('grid');
useValue returns the control's value when the experiment is not running, so the page never breaks when a test is paused.
Hooks
| Hook | Returns |
|---|---|
useVariant(id) |
the variant key ('control', 'bold'…); records an exposure the first time |
useValue(id) |
the payload of a value experiment |
useTrack() |
(name, props?) => void: an event, any name |
useIdentify() |
(userId, traits?) => void: ties the visitor to your user; null on sign-out |
usePageview(path) |
reports a page yourself, for when automatic navigation tracking is off |
useSplitHub() |
the underlying @splithub/core client |
useVariant is for the same experiment in several places: the visitor gets one variant everywhere.
Goals
const track = useTrack();
return (
<>
<button onClick={() => track('signup')}>Sign up</button>
<button onClick={() => track('purchase', { revenue: 49 })}>Buy</button>
</>
);
Pageview goals fire for the current path on load and on every client-side navigation; there is nothing to add to your router. Click goals are bound automatically from the config's selectors.
Analytics
Name an analytics property on the provider and the same install collects pageviews and events. npx splithub analytics connect adds the prop for you.
<SplitHubProvider config={config} property="sh-x1h78w">
<App />
</SplitHubProvider>
- Pageviews are automatic: one on load and one on every client-side navigation (
pushState,replaceState, back and forward, hash routes), with time on page and scroll depth. To report pages yourself, passtrackNavigation={false}and callusePageview(pathname)in the layout. - Events:
useTrack()is the one call.track('Video Played', { id: 3 })records the event in the property and converts every experiment whose goal it is, in one row. Names are letters, digits, spaces and_ . : / -, up to 64 characters; props are a flat object up to 4 KB. - Users: call
identifyafter sign-in and withnullon sign-out.
import { useEffect } from 'react';
import { useIdentify } from '@splithub/react';
export function IdentifyUser({ user }: { user: { id: string; plan: string } | null }) {
const identify = useIdentify();
useEffect(() => {
if (user) identify(user.id, { plan: user.plan });
else identify(null);
}, [identify, user]);
return null;
}
Use your internal user id; keep emails and names out of traits and props. A development build (NODE_ENV=development) sends nothing to SplitHub Cloud: check tracking on a deploy or with vite preview.
QA
Append ?sh_force=hero-cta:bold to any URL to see a variant, running or draft; no exposure is recorded. Keep the parameter in the URL while previewing; it is not saved across navigation. Several at once: ?sh_force=hero-cta:bold,hero-title:outcome.
To run drafts while you develop without forcing anything, set runDraftsInDev: true in the config (see Drafts in development). Add ?sh_debug=1, or run npx splithub dev, to log every assignment with its reason and get a panel that switches variants. In Vite apps, splithub() from @splithub/core/vite prints what each experiment does in your dev terminal (see Drafts in development).
SSR
Any framework that renders on the server: read the sh_vid cookie, pass it as visitorId to the provider on both sides, set the cookie on the response when missing. @splithub/core exports newVisitorId() and assign() for server-side decisions.
Rules
- Never nest an
<Experiment>inside a<Variant>of the same experiment. - Hooks follow the rules of hooks: no conditional calls.
- Keep the config import at the top of the app; the provider must be above every component that uses a hook.