Feature Flags
Feature flags in our application are managed through Amplitude Experiment. This system allows us to control feature rollouts, A/B testing, and gradual feature releases.
Usage
Basic Usage
To use a feature flag in a component:
import { useExperiment } from "@repo/amplitude-frontend/client"
function MyComponent() {
const { isOn, value, payload, variant, isLoading, error } = useExperiment({
flagKey: "my-feature-flag",
fallbackValue: "off", // Optional, defaults to "off"
})
if (isLoading) {
return <LoadingSpinner />
}
if (error) {
return <ErrorMessage error={error} />
}
if (!isOn) {
return null
}
return <div>Feature is enabled!</div>
}Available Properties
The useExperiment hook returns:
isOn: Boolean indicating if the feature is enabled (value === “on”)value: The raw value of the flagpayload: Any additional data associated with the flagisLoading: Boolean indicating if the flag is still being fetchederror: Any error that occurred during fetchingvariant: The full variant object
Using Feature Flags in Callbacks
For cases where you need to check feature flags inside callbacks, effects, or other places where React hooks can’t be used directly, use the useExperimentActions hook:
import { useExperimentActions } from "@repo/amplitude-frontend/client"
function MyComponent() {
const { hasVariant, getVariant } = useExperimentActions()
// Safe to use in callbacks, useMemo, etc.
const filteredItems = useMemo(() =>
items.filter(item => {
return hasVariant('show-experimental-items') ? true : !item.experimental
}),
[items, hasVariant]
)
// Access full variant data if needed
const handleClick = useCallback(() => {
const variant = getVariant('special-feature')
if (variant.value === 'beta') {
// Handle beta variant
}
}, [getVariant])
return <div>{/* ... */}</div>
}The useExperimentActions hook provides:
hasVariant(flagKey, fallbackValue?): Returns a boolean indicating if the feature is enabledgetVariant(flagKey, fallbackValue?): Returns the full variant object
Best Practices
-
Always Provide a Fallback Value
const { isOn } = useExperiment({ flagKey: "my-feature", fallbackValue: "off", // Explicit fallback for SSR and error cases }) -
Use Descriptive Flag Keys
- Use kebab-case for flag keys
- Prefix with feature area (e.g., “import-query-builder”)
- Make the purpose clear from the key name
-
Handle Loading and Error States
const { isOn, isLoading, error } = useExperiment({ flagKey: "my-feature", }) if (isLoading) { return <LoadingSpinner /> } if (error) { return <ErrorMessage error={error} /> } if (!isOn) { return null } -
Choose the Right Hook
- Use
useExperimentfor component-level feature flags - Use
useExperimentActionsfor callbacks and other places where hooks can’t be used directly - Don’t use hooks inside callbacks or effects - use
useExperimentActionsinstead
- Use
-
Local Variant Optimization The hooks automatically check for local variants before making network requests, optimizing performance.
Example Implementation
Here’s a real-world example from our codebase:
// Using useExperiment for component-level flags
const { isOn: isQueryBuilderEnabled, isLoading } = useExperiment({
flagKey: "import-query-builder",
fallbackValue: "off",
})
// Using useExperimentActions for filtering
const { hasVariant } = useExperimentActions()
const filteredAttributes = useMemo(
() => attributes.filter(attr => !attr.featureFlag || hasVariant(attr.featureFlag)),
[attributes, hasVariant]
)
return (
<>
{isLoading ? (
<LoadingSpinner />
) : (
<Button
mode="outline"
onClick={handleClick}
disabled={!isQueryBuilderEnabled}
>
{t("global.common.addCardsWithQueryBuilder")}
</Button>
)}
</>
)Flag States
Feature flags can have the following states:
"on": Feature is enabled"off": Feature is disabled- Custom values: For A/B testing or gradual rollouts
SSR Considerations
The hooks are SSR-safe and will:
- Return the fallback value during server-side rendering
- Check for local variants before making network requests
- Handle hydration gracefully
Performance
- Local variants are checked first to avoid unnecessary network requests
- Flags are cached locally
- Network requests are only made when necessary
useExperimentActionsprovides stable references for optimal performance
Creating New Flags
- Create the flag in Amplitude Experiment dashboard
- Use kebab-case for the flag key
- Set appropriate default values
- Document the flag’s purpose and expected values
Feature Flag Definitions
All feature flags used in the application must be defined in the TypeScript type definition file at packages/integrations/amplitude-shared/src/feature-flags.ts. This ensures type safety and prevents runtime errors from undefined flag keys.
The feature flags are defined as a union type:
export type FeatureFlag =
| "list-visibility-toggles"
| "graded-cards"
| "..."Testing
When testing components with feature flags:
- Test both enabled and disabled states
- Verify fallback behavior
- Check loading and error states
- Verify SSR behavior
- Test callback behavior with
useExperimentActions
Troubleshooting
Common issues and solutions:
-
Flag not working in SSR
- Ensure fallbackValue is provided
- Check that the component is properly marked with “use client”
-
Flag not updating
- Verify the flag key is correct
- Check Amplitude dashboard for flag configuration
- Clear local storage if testing locally
-
Performance issues
- Verify you’re not making unnecessary network requests
- Check that local variants are being used
- Use
useExperimentActionsfor callbacks and filtering
-
Hook rules violations
- Don’t use
useExperimentinside callbacks or effects - Use
useExperimentActionsinstead for these cases - Ensure hooks are only called at the top level of components
- Don’t use