Growthbook
Overviewโ
This third-party service is used for A/B experimentation and feature flags. This product is able to track customer metrics; for example: following a customer journey in an experiment that can lead to a higher add to bag rate that leads to conversion.
Key features:
- Feature Flags
- Experiments (A/B)
Technical Directionโ
When utilizing Growthbook, we want to render the Growthbook experiment on the server-side then pass the data down into our client-side components. Rendering server-side prevents flickering and other React race conditions that can appear "glitchy" to clients.
We're also able to use this deferred loader information in different components in the codebase (if need be), so we don't have to consistently ping the Growthbook API/SDK.
Usageโ
When creating a new feature flag or experiment, we need to add the new type to the growthbook.d.ts file with it's respective type. See more about feature types here.
General rule of thumb, if the experiment is A/B (two variants; one control, one experiment), we can use boolean. If we are testing multiple variants, A/B/C, we can use string and set the values as the respective letters. In the AppFeatures map, we should strictly use the values being set instead of string. See example below:
export type AppFeatures = {
'cart_survey-role-form_2026-01': boolean;
'pdp_color-selector-placement_2026_01': 'A' | 'B' | 'C';
'lp_hide-announcement-site-switcher_2026_01': 'A' | 'B' | 'C';
...
};
Server-sideโ
getGrowthbookFeatureโ
This server-side helper function is created to utilize GB's getFeatureValue function (see here), which we can later use to insert our own variables and arguments.
// app/lib/growthbook/helper.server.ts
export const getGrowthbookFeature = <FeatureType extends keyof AppFeatures>(
growthbook: GrowthBook,
featureKey: FeatureType,
defaultValue: AppFeatures[FeatureType]
) => growthbook.getFeatureValue(featureKey, defaultValue);
How it's used in loader:
// products.$handle.tsx
const enableDefaultStandardProduction = getGrowthbookFeature(args.context.growthbook, 'pdp-default-mod', false);
const enableRemoveProductionSpeedLabels = getGrowthbookFeature(
args.context.growthbook,
'remove-production-speed-labels',
false
);
const enableColorSelectorPlacement = getGrowthbookFeature(
args.context.growthbook,
'pdp_color-selector-placement_2026_01',
'A'
);
/**
* Additional loader logic here ...
*/
return {
// other loader variables
enableDefaultStandardProduction,
enableRemoveProductionSpeedLabels,
enableColorSelectorPlacement
};
Client-sideโ
After this data is brought in via useLoaderData, we take this variable and pass it down to the component. An example of this being used in PDP:
// products.$handle.tsx
/**
* Adding these feature flags to the provider in this case, allows us to access
* these variables all across the PDPs. Depending on what the experiment needs
* are, you may be able to call the feature value in a more isolated way.
*/
<PdpProvider
enableDefaultStandardProduction={enableDefaultStandardProduction}
enableRemoveProductionSpeedLabels={enableRemoveProductionSpeedLabels}
enableColorSelectorPlacement={enableColorSelectorPlacement} // <--
>
{children}
</PdpProvider>
// Example of this variable being used in a smaller reusable component:
<div
dangerouslySetInnerHTML={{ __html: description }}
className={clsx(enableColorSelectorPlacement === 'C' && 'hidden')}
/>
Naturally, we'll want to send the analytics of these experiments to GTM and Heap. Luckily Growthbook's functions do provide this in useFeature. We wrap this hook in our own custom hook called useSendGBAnalyticsEvent.
function useSendGBAnalyticsEvent(feature: keyof AppFeatures) {
useFeature(feature);
}
How to use this hook:
// products.$handle.tsx
const ProductDetailPage: FC = () => {
const { enableColorSelectorPlacement } = useLoaderData<typeof loader>();
/**
* Insert directly within the component so it runs on mount. This helps us track
* which experiment variant is assigned to this client ID. When a client navigates
* away, GB will persist that value, so when they return to the experiment page,
* it is a consistent experience.
*/
useSendGBAnalyticsEvent('pdp_color-selector-placement_2026_01');
...
}
"Conditional" useSendGBAnalyticsโ
Some experiments require that we run our analytics hooks only on specific sections. One example is if we want to experiment on a landing page that has multiple variants (e.g. /pages/landing-page-1, /pages/landing-page-2), and we decide to add the analytics to PageLayout to account for any pages with /pages/landing-page in their pathname, we have to conditionally render a hook.
However, it is a React no-no to "conditionally" render hook (see docs here), so we've created a small reusable component called ConditionalSiteAnalytics.
// app/lib/experiments/helper.tsx
export const ConditionalSiteAnalytics = ({ feature }: { feature: keyof AppFeatures }) => {
useSendGBAnalyticsEvent(feature);
return null;
};
Example of the conditional hook in a client-side component:
// app/components/PageLayout.tsx
export function PageLayout({ props }) {
return (
<main>
{isSwatchLP && (
<ConditionalSiteAnalytics feature={'lp_hide-announcement-site-switcher_2026_01' as keyof AppFeatures} />
)}
</main>
)
}
We can later test this in our DevTools and logging window.dataLayer and see if the custom GTM events are being fired โ they should have growthbook prepended with the experiment ID/name.
Troubleshootingโ
- If you're not seeing the feature appear in the Growthbook extension:
- Make sure the experiment is running. If it's not production ready, uncheck
productionbut keepqaandstagingactive.
- Make sure the experiment is running. If it's not production ready, uncheck
- If you're not able to see the feature appear in the Growthbook extension, despite the experiment running:
- Try to clear the extension cache in DevTools.