/** * React context provider for SSR-hydrated feature-flag results. * * Pattern (matches the spec): the root loader pre-evaluates every flag * the route tree needs and serialises the result map into loaderData. * The provider lifts that map into context; `useFeatureFlag(name)` * returns the synchronous result without re-fetching. * * Hydration shape: * ``` * { [flagName]: { enabled, variant?, reason } } * ``` * * Falls back to `{ enabled: false, reason: "flag-not-found" }` for any * name not present in the hydrated map — safer than throwing because a * stale client bundle (deployment skew) won't crash the page when a * new flag is referenced. */ import { createContext, useContext, useMemo, type ReactNode } from "react"; import type { EvaluateFlagResult } from "../evaluate.js"; export type FeatureFlagsHydration = Readonly>; interface FeatureFlagContextValue { readonly flags: FeatureFlagsHydration; } const FALLBACK: EvaluateFlagResult = Object.freeze({ enabled: false, reason: "flag-not-found", }); const Context = createContext({ flags: {} }); export interface FeatureFlagProviderProps { readonly flags: FeatureFlagsHydration; readonly children?: ReactNode; } /** Wraps the React tree and exposes the hydrated flag map to {@link useFeatureFlag}. */ export function FeatureFlagProvider(props: FeatureFlagProviderProps): JSX.Element { const value = useMemo(() => ({ flags: props.flags }), [props.flags]); return {props.children}; } /** * Read a single flag from the hydrated context. * * Returns the `{ enabled, variant?, reason }` triple. A `null`-style * boolean shortcut is intentionally NOT exposed — callers should * destructure so future variant-callers don't have to refactor. */ export function useFeatureFlag(name: string): EvaluateFlagResult { const ctx = useContext(Context); return ctx.flags[name] ?? FALLBACK; } /** * Convenience: returns just the boolean. For call-sites that only * care about on/off and don't need variants or reasons. */ export function useFeatureFlagEnabled(name: string): boolean { return useFeatureFlag(name).enabled; } /** * Returns the variant key (or undefined). Use for A/B branches. */ export function useFeatureFlagVariant(name: string): string | undefined { return useFeatureFlag(name).variant?.key; }