Skip to Content
FrontendFeature Flags

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 flag
  • payload: Any additional data associated with the flag
  • isLoading: Boolean indicating if the flag is still being fetched
  • error: Any error that occurred during fetching
  • variant: 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 enabled
  • getVariant(flagKey, fallbackValue?): Returns the full variant object

Best Practices

  1. Always Provide a Fallback Value

    const { isOn } = useExperiment({ flagKey: "my-feature", fallbackValue: "off", // Explicit fallback for SSR and error cases })
  2. 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
  3. 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 }
  4. Choose the Right Hook

    • Use useExperiment for component-level feature flags
    • Use useExperimentActions for callbacks and other places where hooks can’t be used directly
    • Don’t use hooks inside callbacks or effects - use useExperimentActions instead
  5. 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:

  1. Return the fallback value during server-side rendering
  2. Check for local variants before making network requests
  3. 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
  • useExperimentActions provides stable references for optimal performance

Creating New Flags

  1. Create the flag in Amplitude Experiment dashboard
  2. Use kebab-case for the flag key
  3. Set appropriate default values
  4. 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:

  1. Test both enabled and disabled states
  2. Verify fallback behavior
  3. Check loading and error states
  4. Verify SSR behavior
  5. Test callback behavior with useExperimentActions

Troubleshooting

Common issues and solutions:

  1. Flag not working in SSR

    • Ensure fallbackValue is provided
    • Check that the component is properly marked with “use client”
  2. Flag not updating

    • Verify the flag key is correct
    • Check Amplitude dashboard for flag configuration
    • Clear local storage if testing locally
  3. Performance issues

    • Verify you’re not making unnecessary network requests
    • Check that local variants are being used
    • Use useExperimentActions for callbacks and filtering
  4. Hook rules violations

    • Don’t use useExperiment inside callbacks or effects
    • Use useExperimentActions instead for these cases
    • Ensure hooks are only called at the top level of components
Last updated on