Files
HiveGrid-Wireframes/hivegrid-cloud/packages/feature-flags/src/client/context.tsx
T

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;
}