@@ -6,9 +6,11 @@ import {
66 JsonValue ,
77 ProviderEvents ,
88 ProviderStatus ,
9+ StandardResolutionReasons ,
910} from '@openfeature/web-sdk' ;
1011import { Dispatch , SetStateAction , useEffect , useState } from 'react' ;
1112import { useOpenFeatureClient } from '../provider' ;
13+ import { FlagQuery } from '../query' ;
1214
1315type ReactFlagEvaluationOptions = {
1416 /**
@@ -52,9 +54,64 @@ enum SuspendState {
5254 Error ,
5355}
5456
57+ // This type is a bit wild-looking, but I think we need it.
58+ // We have to use the conditional, because otherwise useFlag('key', false) would return false, not boolean (too constrained).
59+ // We have a duplicate for the hook return below, this one is just used for casting because the name isn't as clear
60+ type ConstrainedFlagQuery < T > = FlagQuery <
61+ T extends boolean
62+ ? boolean
63+ : T extends number
64+ ? number
65+ : T extends string
66+ ? string
67+ : T extends JsonValue
68+ ? T
69+ : JsonValue
70+ > ;
71+
72+ /**
73+ * Evaluates a feature flag generically, returning an react-flavored queryable object.
74+ * The resolver method to use is based on the type of the defaultValue.
75+ * For type-specific hooks, use {@link useBooleanFlagValue}, {@link useBooleanFlagDetails} and equivalents.
76+ * By default, components will re-render when the flag value changes.
77+ * @param {string } flagKey the flag identifier
78+ * @template {FlagValue} T A optional generic argument constraining the default.
79+ * @param {T } defaultValue the default value; used to determine what resolved type should be used.
80+ * @param {ReactFlagEvaluationOptions } options for this evaluation
81+ * @returns { FlagQuery } a queryable object containing useful information about the flag.
82+ */
83+ export function useFlag < T extends FlagValue = FlagValue > (
84+ flagKey : string ,
85+ defaultValue : T ,
86+ options ?: ReactFlagEvaluationOptions ,
87+ ) : FlagQuery <
88+ T extends boolean
89+ ? boolean
90+ : T extends number
91+ ? number
92+ : T extends string
93+ ? string
94+ : T extends JsonValue
95+ ? T
96+ : JsonValue
97+ > {
98+ // use the default value to determine the resolver to call
99+ const query =
100+ typeof defaultValue === 'boolean'
101+ ? new HookFlagQuery < boolean > ( useBooleanFlagDetails ( flagKey , defaultValue , options ) )
102+ : typeof defaultValue === 'number'
103+ ? new HookFlagQuery < number > ( useNumberFlagDetails ( flagKey , defaultValue , options ) )
104+ : typeof defaultValue === 'string'
105+ ? new HookFlagQuery < string > ( useStringFlagDetails ( flagKey , defaultValue , options ) )
106+ : new HookFlagQuery < JsonValue > ( useObjectFlagDetails ( flagKey , defaultValue , options ) ) ;
107+ // TS sees this as HookFlagQuery<JsonValue>, because the compiler isn't aware of the `typeof` checks above.
108+ return query as unknown as ConstrainedFlagQuery < T > ;
109+ }
110+
55111/**
56112 * Evaluates a feature flag, returning a boolean.
57113 * By default, components will re-render when the flag value changes.
114+ * For a generic hook returning a queryable interface, see {@link useFlag}.
58115 * @param {string } flagKey the flag identifier
59116 * @param {boolean } defaultValue the default value
60117 * @param {ReactFlagEvaluationOptions } options options for this evaluation
@@ -71,6 +128,7 @@ export function useBooleanFlagValue(
71128/**
72129 * Evaluates a feature flag, returning evaluation details.
73130 * By default, components will re-render when the flag value changes.
131+ * For a generic hook returning a queryable interface, see {@link useFlag}.
74132 * @param {string } flagKey the flag identifier
75133 * @param {boolean } defaultValue the default value
76134 * @param {ReactFlagEvaluationOptions } options options for this evaluation
@@ -94,6 +152,7 @@ export function useBooleanFlagDetails(
94152/**
95153 * Evaluates a feature flag, returning a string.
96154 * By default, components will re-render when the flag value changes.
155+ * For a generic hook returning a queryable interface, see {@link useFlag}.
97156 * @param {string } flagKey the flag identifier
98157 * @template {string} [T=string] A optional generic argument constraining the string
99158 * @param {T } defaultValue the default value
@@ -104,13 +163,14 @@ export function useStringFlagValue<T extends string = string>(
104163 flagKey : string ,
105164 defaultValue : T ,
106165 options ?: ReactFlagEvaluationOptions ,
107- ) : T {
166+ ) : string {
108167 return useStringFlagDetails ( flagKey , defaultValue , options ) . value ;
109168}
110169
111170/**
112171 * Evaluates a feature flag, returning evaluation details.
113172 * By default, components will re-render when the flag value changes.
173+ * For a generic hook returning a queryable interface, see {@link useFlag}.
114174 * @param {string } flagKey the flag identifier
115175 * @template {string} [T=string] A optional generic argument constraining the string
116176 * @param {T } defaultValue the default value
@@ -121,7 +181,7 @@ export function useStringFlagDetails<T extends string = string>(
121181 flagKey : string ,
122182 defaultValue : T ,
123183 options ?: ReactFlagEvaluationOptions ,
124- ) : EvaluationDetails < T > {
184+ ) : EvaluationDetails < string > {
125185 return attachHandlersAndResolve (
126186 flagKey ,
127187 defaultValue ,
@@ -135,6 +195,7 @@ export function useStringFlagDetails<T extends string = string>(
135195/**
136196 * Evaluates a feature flag, returning a number.
137197 * By default, components will re-render when the flag value changes.
198+ * For a generic hook returning a queryable interface, see {@link useFlag}.
138199 * @param {string } flagKey the flag identifier
139200 * @template {number} [T=number] A optional generic argument constraining the number
140201 * @param {T } defaultValue the default value
@@ -145,13 +206,14 @@ export function useNumberFlagValue<T extends number = number>(
145206 flagKey : string ,
146207 defaultValue : T ,
147208 options ?: ReactFlagEvaluationOptions ,
148- ) : T {
209+ ) : number {
149210 return useNumberFlagDetails ( flagKey , defaultValue , options ) . value ;
150211}
151212
152213/**
153214 * Evaluates a feature flag, returning evaluation details.
154215 * By default, components will re-render when the flag value changes.
216+ * For a generic hook returning a queryable interface, see {@link useFlag}.
155217 * @param {string } flagKey the flag identifier
156218 * @template {number} [T=number] A optional generic argument constraining the number
157219 * @param {T } defaultValue the default value
@@ -162,7 +224,7 @@ export function useNumberFlagDetails<T extends number = number>(
162224 flagKey : string ,
163225 defaultValue : T ,
164226 options ?: ReactFlagEvaluationOptions ,
165- ) : EvaluationDetails < T > {
227+ ) : EvaluationDetails < number > {
166228 return attachHandlersAndResolve (
167229 flagKey ,
168230 defaultValue ,
@@ -176,6 +238,7 @@ export function useNumberFlagDetails<T extends number = number>(
176238/**
177239 * Evaluates a feature flag, returning an object.
178240 * By default, components will re-render when the flag value changes.
241+ * For a generic hook returning a queryable interface, see {@link useFlag}.
179242 * @param {string } flagKey the flag identifier
180243 * @template {JsonValue} [T=JsonValue] A optional generic argument describing the structure
181244 * @param {T } defaultValue the default value
@@ -193,6 +256,7 @@ export function useObjectFlagValue<T extends JsonValue = JsonValue>(
193256/**
194257 * Evaluates a feature flag, returning evaluation details.
195258 * By default, components will re-render when the flag value changes.
259+ * For a generic hook returning a queryable interface, see {@link useFlag}.
196260 * @param {string } flagKey the flag identifier
197261 * @param {T } defaultValue the default value
198262 * @template {JsonValue} [T=JsonValue] A optional generic argument describing the structure
@@ -336,3 +400,52 @@ function suspenseWrapper<T>(promise: Promise<T>) {
336400 }
337401 } ;
338402}
403+
404+ // FlagQuery implementation, do not export
405+ class HookFlagQuery < T extends FlagValue = FlagValue > implements FlagQuery {
406+ constructor ( private _details : EvaluationDetails < T > ) { }
407+
408+ get details ( ) {
409+ return this . _details ;
410+ }
411+
412+ get value ( ) {
413+ return this . _details ?. value ;
414+ }
415+
416+ get variant ( ) {
417+ return this . _details . variant ;
418+ }
419+
420+ get flagMetadata ( ) {
421+ return this . _details . flagMetadata ;
422+ }
423+
424+ get reason ( ) {
425+ return this . _details . reason ;
426+ }
427+
428+ get isError ( ) {
429+ return ! ! this . _details ?. errorCode || this . _details . reason == StandardResolutionReasons . ERROR ;
430+ }
431+
432+ get errorCode ( ) {
433+ return this . _details ?. errorCode ;
434+ }
435+
436+ get errorMessage ( ) {
437+ return this . _details ?. errorMessage ;
438+ }
439+
440+ get isAuthoritative ( ) {
441+ return (
442+ ! this . isError &&
443+ this . _details . reason != StandardResolutionReasons . STALE &&
444+ this . _details . reason != StandardResolutionReasons . DISABLED
445+ ) ;
446+ }
447+
448+ get type ( ) {
449+ return typeof this . _details . value ;
450+ }
451+ }
0 commit comments