-
Notifications
You must be signed in to change notification settings - Fork 42
feat(react): add FeatureFlag component #1164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c487cd1
0213bed
0f8e277
daf458d
1ada096
cd10e6f
db70768
04b76e2
d48c683
d403fae
20978c1
dc79d47
646c879
b4118cb
fa719b6
da1f737
0428b0d
af4c8d5
f5b6788
8867f35
5501404
5c86989
a9e837c
7e2a753
51037db
e9f2c41
7bbde6e
3a3eb8b
fadecb1
9c5f14d
b03c27c
ba2c761
4e335ac
7d2fb3a
5e276c9
81ce66a
763c513
461d9dc
4fa96df
d92ffcd
5c1b3d6
b475a6e
7f95d1e
a806eac
035e377
ec0cffa
f7a18fb
06fd1e4
5b9d59c
d00fc09
b0fd0d7
c24b83f
17d0c95
0aa2db9
7398a9b
70e6407
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import React from 'react'; | ||
import { useFlag } from '../evaluation'; | ||
import type { FlagQuery } from '../query'; | ||
import type { FlagValue, EvaluationDetails } from '@openfeature/core'; | ||
|
||
/** | ||
* Default predicate function that checks if the expected value equals the actual flag value. | ||
* @param {T} expected The expected value to match against | ||
* @param {EvaluationDetails<T>} actual The evaluation details containing the actual flag value | ||
* @returns {boolean} true if the values match, false otherwise | ||
*/ | ||
function equals<T extends FlagValue>(expected: T, actual: EvaluationDetails<T>): boolean { | ||
return expected === actual.value; | ||
} | ||
|
||
/** | ||
* Props for the FeatureFlag component that conditionally renders content based on feature flag state. | ||
* @interface FeatureFlagProps | ||
*/ | ||
interface FeatureFlagProps<T extends FlagValue = FlagValue> { | ||
/** | ||
* The key of the feature flag to evaluate. | ||
*/ | ||
flagKey: string; | ||
|
||
/** | ||
* Optional value to match against the feature flag value. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please specify here exactly what comparison is used\ ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we do the default mentioned below, we can just describe it as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in the jsdoc comment |
||
* If provided, the component will only render children when the flag value matches this value. | ||
* By default, strict equality (===) is used for comparison. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this be overridden? The phrase "by default" implies that it can be but I'm not show how. |
||
* If a boolean, it will check if the flag is enabled (true) or disabled (false). | ||
* If a string, it will check if the flag variant equals this string. | ||
*/ | ||
match?: T; | ||
|
||
/** | ||
* Optional predicate function for custom matching logic. | ||
* If provided, this function will be used instead of the default equality check. | ||
* @param expected The expected value (from match prop) | ||
* @param actual The evaluation details | ||
* @returns true if the condition is met, false otherwise | ||
*/ | ||
predicate?: (expected: T | undefined, actual: EvaluationDetails<T>) => boolean; | ||
|
||
/** | ||
* Default value to use when the feature flag is not found. | ||
*/ | ||
defaultValue: T; | ||
|
||
/** | ||
* Content to render when the feature flag condition is met. | ||
* Can be a React node or a function that receives flag query details and returns a React node. | ||
*/ | ||
children: React.ReactNode | ((details: FlagQuery<T>) => React.ReactNode); | ||
|
||
/** | ||
* Optional content to render when the feature flag condition is not met. | ||
* Can be a React node or a function that receives evaluation details and returns a React node. | ||
*/ | ||
fallback?: React.ReactNode | ((details: EvaluationDetails<T>) => React.ReactNode); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also consider an optional loading component? This may have already been discussed but I didn't notice it in the thread yet. |
||
} | ||
|
||
/** | ||
* FeatureFlag component that conditionally renders its children based on the evaluation of a feature flag. | ||
* @param {FeatureFlagProps} props The properties for the FeatureFlag component. | ||
* @returns {React.ReactElement | null} The rendered component or null if the feature is not enabled. | ||
*/ | ||
export function FeatureFlag<T extends FlagValue = FlagValue>({ | ||
flagKey, | ||
match, | ||
predicate, | ||
defaultValue, | ||
children, | ||
fallback = null, | ||
}: FeatureFlagProps<T>): React.ReactElement | null { | ||
const details = useFlag(flagKey, defaultValue, { | ||
updateOnContextChanged: true, | ||
}); | ||
|
||
// If the flag evaluation failed, we render the fallback | ||
if (details.reason === 'ERROR') { | ||
const fallbackNode: React.ReactNode = typeof fallback === 'function' ? fallback(details.details as EvaluationDetails<T>) : fallback; | ||
return <>{fallbackNode}</>; | ||
} | ||
|
||
// Use custom predicate if provided, otherwise use default matching logic | ||
let shouldRender = false; | ||
if (predicate) { | ||
shouldRender = predicate(match, details.details as EvaluationDetails<T>); | ||
} else if (match !== undefined) { | ||
// Default behavior: check if match value equals flag value | ||
shouldRender = equals(match, details.details as EvaluationDetails<T>); | ||
} else { | ||
// If no match value is provided, render if flag is truthy | ||
shouldRender = Boolean(details.value); | ||
} | ||
|
||
if (shouldRender) { | ||
const childNode: React.ReactNode = typeof children === 'function' ? children(details as FlagQuery<T>) : children; | ||
return <>{childNode}</>; | ||
} | ||
|
||
const fallbackNode: React.ReactNode = typeof fallback === 'function' ? fallback(details.details as EvaluationDetails<T>) : fallback; | ||
return <>{fallbackNode}</>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './FeatureFlag'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be better to reuse this
is-equal
check.https://github.com/open-feature/js-sdk/blob/main/packages/react/src/internal/is-equal.ts