Setting Up a Growthbook Experiment
Feature Flag Typesโ
When creating a new feature flag or experiment, add the new feature flag meta data on the GROWTHBOOK_GATES_REGISTRY in app/lib/growthbook/registry.ts. See more about feature types here.
General rule of thumb: if the experiment is A/B (two variants; one control, one experiment), use boolean. If testing multiple variants (A/B/C), use a union of the variant letters.
export const GROWTHBOOK_GATES_REGISTRY = {
'h2_[xxx-xxx]': {
defaultValue: false,
name: 'Enable Dedicated Builder.io Space for Homepage',
description:
'Enables using a dedicated Builder.io space for homepage content (/ and /suits routes) for billing optimization',
scopeOfImpact: 'Homepage (/) and Suits Homepage (/suits)',
},
'h2_[xxx-xxx]': {
defaultValue: false,
name: 'Manually resolve Shopify Products for Builder content',
description: 'In case Builder content stops resolving Shopify Products, resolve it in Hydrogen loader',
scopeOfImpact: 'Global',
},
//....
'lp_[xxx-xxx-xxx]_2026_02': {
defaultValue: 'A',
name: 'Enable [xxx] in Swatch LP',
description:
'This flag is associated with experiments relating to the Swatch LP. This A/B/C flag tests different variations of the [xxx] component. Please see full test details on GrowthBook.',
scopeOfImpact: 'Swatch LP',
}
}
Server-side: checkGateโ
This function is stored in registry.ts and writes to the window.GATES to update client-side storage and on the server-side, we update to the latest feature flag values in server.ts.
On the server, we update our static defauilt values from registry.ts with GB's getFeatureValue function (see here) and it returns readable feature flag value for our loaders.
// app/lib/growthbook/helper.server.ts
export const getGrowthbookFeature = <FeatureKey extends keyof AppFeatures>(
growthbook: GrowthBook,
key: FeatureKey,
fallback?: AppFeatures[FeatureKey]
): AppFeatures[FeatureKey] => {
return growthbook.getFeatureValue(key, fallback ?? GATES_REGISTRY[key]) as AppFeatures[FeatureKey];
};
How it's used in a loader:
// products.$handle.tsx
const enableColorSelectorPlacement = checkGate('pdp_[xxx-xxx-xxx]_2026_01')
return {
// other loader variables
enableColorSelectorPlacement,
};
Client-sideโ
Passing the flag to componentsโ
After the feature flag value is brought in via useLoaderData, pass it down to the relevant component. Depending on the experiment's scope, this may be directly into a provider (for broad access across a page) or into a more isolated component.
// products.$handle.tsx
<PdpProvider
enableColorSelectorPlacement={enableColorSelectorPlacement}
>
{children}
</PdpProvider>
// Example of the variable being used in a smaller reusable component:
<div
dangerouslySetInnerHTML={{ __html: description }}
className={clsx(enableColorSelectorPlacement === 'C' && 'hidden')}
/>
Analyticsโ
Add useSendGBAnalyticsEvent at the top level of the page component so the variant assignment is tracked on mount. When a client navigates away, GB persists that value so they get a consistent experience on return.
// products.$handle.tsx
const ProductDetailPage: FC = () => {
useSendGBAnalyticsEvent('pdp_[xxx-xxx-xxx]_2026_01');
// ...
};
Conditional analyticsโ
Some experiments require the analytics hook to only run in specific contexts (e.g. a subset of pages). Since conditionally calling a hook directly violates the rules of hooks, use the ConditionalSiteAnalytics component instead:
// app/lib/experiments/conditionalAnalytics.tsx
export const ConditionalSiteAnalytics = ({ feature }: { feature: keyof AppFeatures }) => {
useSendGBAnalyticsEvent(feature);
return null;
};
// app/components/PageLayout.tsx
export function PageLayout({ props }) {
return (
<main>
{isSwatchLP && (
<ConditionalSiteAnalytics feature='lp_hide-announcement-site-switcher_2026_01' />
)}
</main>
);
}
You can verify events are firing by opening DevTools and logging window.dataLayer โ events should have growthbook prepended with the experiment ID/name.