73 lines
2.4 KiB
TypeScript
73 lines
2.4 KiB
TypeScript
/**
|
|
* 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<Record<string, EvaluateFlagResult>>;
|
|
|
|
interface FeatureFlagContextValue {
|
|
readonly flags: FeatureFlagsHydration;
|
|
}
|
|
|
|
const FALLBACK: EvaluateFlagResult = Object.freeze({
|
|
enabled: false,
|
|
reason: "flag-not-found",
|
|
});
|
|
|
|
const Context = createContext<FeatureFlagContextValue>({ 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<FeatureFlagContextValue>(() => ({ flags: props.flags }), [props.flags]);
|
|
return <Context.Provider value={value}>{props.children}</Context.Provider>;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|