Skip to main content

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.