Skip to content

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

Open
wants to merge 56 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
c487cd1
feat(react): add FeatureFlag component
weyert Apr 9, 2025
0213bed
feat: add polyfill for react use hook (#1157)
beeme1mr Apr 10, 2025
0f8e277
chore: use server src not dist in nest tests (#1166)
toddbaert Apr 10, 2025
daf458d
chore(main): release core 1.8.0 (#1155)
openfeaturebot Apr 10, 2025
1ada096
feat(angular): add option for initial context injection
lukas-reining Apr 11, 2025
cd10e6f
chore(main): release angular-sdk 0.0.11 (#1167)
openfeaturebot Apr 11, 2025
db70768
chore(deps): update angular-eslint monorepo to v19 (major) (#1140)
renovate[bot] Apr 11, 2025
04b76e2
chore(deps): update dependency esbuild to ^0.25.0 [security] (#1145)
renovate[bot] Apr 11, 2025
d48c683
feat(angular): add docs for setting evaluation context in angular (#1…
lukas-reining Apr 11, 2025
d403fae
chore(main): release angular-sdk 0.0.12 (#1171)
openfeaturebot Apr 11, 2025
20978c1
chore(main): release nestjs-sdk 0.2.3 (#1144)
openfeaturebot Apr 11, 2025
dc79d47
chore(main): release server-sdk 1.18.0 (#1153)
openfeaturebot Apr 11, 2025
646c879
chore(main): release web-sdk 1.5.0 (#1156)
openfeaturebot Apr 14, 2025
b4118cb
chore(main): release react-sdk 1.0.0 (#1154)
openfeaturebot Apr 14, 2025
fa719b6
docs: fix readme typo (#1174)
lukas-reining Apr 14, 2025
da1f737
chore: use publish env
toddbaert Apr 16, 2025
0428b0d
chore(nest): allow nestjs version 11 (#1176)
lukas-reining Apr 20, 2025
af4c8d5
chore(main): release nestjs-sdk 0.2.4 (#1177)
openfeaturebot Apr 20, 2025
f5b6788
chore(main): release angular-sdk 0.0.13 (#1175)
openfeaturebot Apr 20, 2025
8867f35
docs: Clarify the behavior of setProviderAndWait (#1180)
beeme1mr Apr 23, 2025
5501404
feat: adds RequireFlagsEnabled decorator (#1159)
kaushalkapasi Apr 24, 2025
5c86989
chore: regenerate package lock (#1184)
beeme1mr Apr 24, 2025
a9e837c
chore(deps): update dependency @types/node to v20.17.31 (#1138)
renovate[bot] Apr 30, 2025
7e2a753
chore(deps): update dependency @types/node to v22 (#1067)
renovate[bot] May 15, 2025
51037db
chore(deps): update dependency @types/supertest to v6.0.3 (#1169)
renovate[bot] May 15, 2025
e9f2c41
chore(deps): update dependency rollup to v4.40.2 (#1131)
renovate[bot] May 15, 2025
7bbde6e
chore(deps): update dependency @rollup/plugin-typescript to v12 (#1059)
renovate[bot] May 15, 2025
3a3eb8b
chore(deps): update dependency esbuild to v0.25.4 (#1185)
renovate[bot] May 15, 2025
fadecb1
chore(deps): update dependency @types/node to v22.15.17 (#1190)
renovate[bot] May 15, 2025
9c5f14d
chore(deps): update dependency eslint-plugin-jsdoc to v50.6.14 (#1191)
renovate[bot] May 15, 2025
b03c27c
fix(angular): add license and url field to package.json
lukas-reining May 25, 2025
ba2c761
chore(main): release angular-sdk 0.0.14 (#1178)
openfeaturebot May 25, 2025
4e335ac
chore(deps): update dependency rxjs to v7.8.2 (#1193)
renovate[bot] May 26, 2025
7d2fb3a
chore(deps): update dependency jest-preset-angular to v14.5.5 (#1192)
renovate[bot] May 26, 2025
5e276c9
fix(angular): update docs (#1200)
lukas-reining May 27, 2025
81ce66a
chore(main): release angular-sdk 0.0.15 (#1201)
openfeaturebot May 27, 2025
763c513
fix: environment for npm publish (#1202)
lukas-reining May 27, 2025
461d9dc
chore(main): release nestjs-sdk 0.2.5 (#1183)
openfeaturebot May 27, 2025
4fa96df
chore: update node to v20+ (#1203)
toddbaert May 29, 2025
d92ffcd
chore(deps): update dependency prettier to v3.5.3 (#1195)
renovate[bot] May 30, 2025
5c1b3d6
chore(deps): update dependency @types/node to v22.15.23 (#1198)
renovate[bot] May 30, 2025
b475a6e
refactor(telemetry): update telemetry attributes and remove unused ev…
beeme1mr Jun 3, 2025
7f95d1e
chore(deps): update dependency esbuild to v0.25.5 (#1204)
renovate[bot] Jun 4, 2025
a806eac
chore(deps): update dependency rollup to v4.41.1 (#1205)
renovate[bot] Jun 4, 2025
035e377
chore(deps): update dependency eslint-plugin-jsdoc to v50.7.0 (#1199)
renovate[bot] Jun 4, 2025
ec0cffa
chore(deps): update dependency rollup-plugin-dts to v6.2.1 (#1196)
renovate[bot] Jun 4, 2025
f7a18fb
chore: update workflows to node 20 (#1209)
toddbaert Jun 4, 2025
06fd1e4
chore(deps): update dependency eslint-plugin-jsdoc to v50.7.1 (#1211)
renovate[bot] Jun 4, 2025
5b9d59c
chore(main): release core 1.8.1 (#1208)
openfeaturebot Jun 26, 2025
d00fc09
feat: add evaluation-scoped hook data (#1216)
beeme1mr Jul 18, 2025
b0fd0d7
feat\!: improve FeatureFlag component API with predicate support
weyert Jul 20, 2025
c24b83f
test: update FeatureFlag tests for new API
weyert Jul 20, 2025
17d0c95
chore: update package-lock.json
weyert Jul 20, 2025
0aa2db9
chore: Merge branch 'main' into add-feature-flag-component
weyert Jul 22, 2025
7398a9b
fix: improve the README file of the React package
weyert Jul 22, 2025
70e6407
feat(react): add function-based fallback support to FeatureFlag compo…
weyert Jul 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 79 additions & 2 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ In addition to the feature provided by the [web sdk](https://openfeature.dev/doc
- [Usage](#usage)
- [OpenFeatureProvider context provider](#openfeatureprovider-context-provider)
- [Evaluation hooks](#evaluation-hooks)
- [Declarative components](#declarative-components)
- [Multiple Providers and Domains](#multiple-providers-and-domains)
- [Re-rendering with Context Changes](#re-rendering-with-context-changes)
- [Re-rendering with Flag Configuration Changes](#re-rendering-with-flag-configuration-changes)
Expand Down Expand Up @@ -170,6 +171,72 @@ const {
} = useBooleanFlagDetails('new-message', false);
```

#### Declarative components

The React SDK includes declarative components for feature flagging that provide a more JSX-native approach to conditional rendering.

##### FeatureFlag Component

The `FeatureFlag` component conditionally renders its children based on feature flag evaluation:

```tsx
import { FeatureFlag } from '@openfeature/react-sdk';

function App() {
return (
<OpenFeatureProvider>
{/* Basic usage - renders children when flag is truthy */}
<FeatureFlag flagKey="new-feature" defaultValue={false}>
<NewFeatureComponent />
</FeatureFlag>

{/* Match specific values */}
<FeatureFlag flagKey="theme" match="dark" defaultValue="light">
<DarkThemeStyles />
</FeatureFlag>

{/* Boolean flag with fallback */}
<FeatureFlag
flagKey="premium-feature"
match={true}
defaultValue={false}
fallback={<UpgradePrompt />}
>
<PremiumContent />
</FeatureFlag>

{/* Custom predicate function for complex matching */}
<FeatureFlag
flagKey="user-segment"
defaultValue=""
predicate={(expected, actual) => actual.value.includes('beta')}
>
<BetaFeatures />
</FeatureFlag>

{/* Function as children for accessing flag details */}
<FeatureFlag flagKey="experiment" defaultValue="control">
{(flagDetails) => (
<ExperimentComponent
variant={flagDetails.value}
reason={flagDetails.details.reason}
/>
)}
</FeatureFlag>
</OpenFeatureProvider>
);
}
```

The `FeatureFlag` component supports the following props:

- **`flagKey`** (required): The feature flag key to evaluate
- **`defaultValue`** (required): Default value when the flag is not available
- **`match`** (optional): Value to match against the flag value. By default, strict equality (===) is used for comparison
- **`predicate`** (optional): Custom function for matching logic that receives the expected value and evaluation details
- **`children`**: Content to render when condition is met (can be JSX or a function receiving flag details)
- **`fallback`** (optional): Content to render when condition is not met

#### Multiple Providers and Domains

Multiple providers can be used by passing a `domain` to the `OpenFeatureProvider`:
Expand Down Expand Up @@ -306,8 +373,8 @@ function MyComponent() {
### Testing

The React SDK includes a built-in context provider for testing.
This allows you to easily test components that use evaluation hooks, such as `useFlag`.
If you try to test a component (in this case, `MyComponent`) which uses an evaluation hook, you might see an error message like:
This allows you to easily test components that use evaluation hooks (such as `useFlag`) or declarative components (such as `FeatureFlag`).
If you try to test a component (in this case, `MyComponent`) which uses feature flags, you might see an error message like:

> No OpenFeature client available - components using OpenFeature must be wrapped with an `<OpenFeatureProvider>`.

Expand All @@ -328,6 +395,16 @@ If you'd like to control the values returned by the evaluation hooks, you can pa
<OpenFeatureTestProvider flagValueMap={{ 'my-boolean-flag': true }}>
<MyComponent />
</OpenFeatureTestProvider>

// testing declarative FeatureFlag components
<OpenFeatureTestProvider flagValueMap={{ 'new-feature': true, 'theme': 'dark' }}>
<FeatureFlag flagKey="new-feature" defaultValue={false}>
<NewFeature />
</FeatureFlag>
<FeatureFlag flagKey="theme" match="dark" defaultValue="light">
<DarkMode />
</FeatureFlag>
</OpenFeatureTestProvider>
```

Additionally, you can pass an artificial delay for the provider startup to test your suspense boundaries or loaders/spinners impacted by feature flags:
Expand Down
104 changes: 104 additions & 0 deletions packages/react/src/declarative/FeatureFlag.tsx
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please specify here exactly what comparison is used\ (==vs ===, etc).

Copy link
Member

Choose a reason for hiding this comment

The 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 By default, strict equality is used

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
* 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);
}

/**
* 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}</>;
}
1 change: 1 addition & 0 deletions packages/react/src/declarative/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './FeatureFlag';
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './declarative';
export * from './evaluation';
export * from './query';
export * from './provider';
Expand Down
Loading
Loading