@openfeature/react-sdk 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,8 +16,8 @@
16
16
  <img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.8.0&color=yellow&style=for-the-badge" />
17
17
  </a>
18
18
  <!-- x-release-please-start-version -->
19
- <a href="https://github.com/open-feature/js-sdk/releases/tag/react-sdk-v1.1.0">
20
- <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v1.1.0&color=blue&style=for-the-badge" />
19
+ <a href="https://github.com/open-feature/js-sdk/releases/tag/react-sdk-v1.2.1">
20
+ <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v1.2.1&color=blue&style=for-the-badge" />
21
21
  </a>
22
22
  <!-- x-release-please-end -->
23
23
  <br/>
@@ -50,6 +50,8 @@ In addition to the feature provided by the [web sdk](https://openfeature.dev/doc
50
50
  - [Usage](#usage)
51
51
  - [OpenFeatureProvider context provider](#openfeatureprovider-context-provider)
52
52
  - [Evaluation hooks](#evaluation-hooks)
53
+ - [Declarative components](#declarative-components)
54
+ - [FeatureFlag Component](#featureflag-component)
53
55
  - [Multiple Providers and Domains](#multiple-providers-and-domains)
54
56
  - [Re-rendering with Context Changes](#re-rendering-with-context-changes)
55
57
  - [Re-rendering with Flag Configuration Changes](#re-rendering-with-flag-configuration-changes)
@@ -87,8 +89,8 @@ yarn add @openfeature/react-sdk @openfeature/web-sdk @openfeature/core
87
89
  The following list contains the peer dependencies of `@openfeature/react-sdk`.
88
90
  See the [package.json](./package.json) for the required versions.
89
91
 
90
- * `@openfeature/web-sdk`
91
- * `react`
92
+ - `@openfeature/web-sdk`
93
+ - `react`
92
94
 
93
95
  ### Usage
94
96
 
@@ -108,13 +110,13 @@ const flagConfig = {
108
110
  on: true,
109
111
  off: false,
110
112
  },
111
- defaultVariant: "on",
113
+ defaultVariant: 'on',
112
114
  contextEvaluator: (context: EvaluationContext) => {
113
115
  if (context.silly) {
114
116
  return 'on';
115
117
  }
116
- return 'off'
117
- }
118
+ return 'off';
119
+ },
118
120
  },
119
121
  };
120
122
 
@@ -146,7 +148,7 @@ function Page() {
146
148
  {showNewMessage ? <p>Welcome to this OpenFeature-enabled React app!</p> : <p>Welcome to this React app.</p>}
147
149
  </header>
148
150
  </div>
149
- )
151
+ );
150
152
  }
151
153
  ```
152
154
 
@@ -163,14 +165,71 @@ const value = useBooleanFlagValue('new-message', false);
163
165
  import { useBooleanFlagDetails } from '@openfeature/react-sdk';
164
166
 
165
167
  // "detailed" boolean flag evaluation
166
- const {
167
- value,
168
- variant,
169
- reason,
170
- flagMetadata
171
- } = useBooleanFlagDetails('new-message', false);
168
+ const { value, variant, reason, flagMetadata } = useBooleanFlagDetails('new-message', false);
169
+ ```
170
+
171
+ #### Declarative components
172
+
173
+ The React SDK includes declarative components for feature flagging that provide a more JSX-native approach to conditional rendering.
174
+
175
+ ##### FeatureFlag Component
176
+
177
+ The `FeatureFlag` component conditionally renders its children based on feature flag evaluation:
178
+
179
+ ```tsx
180
+ import { FeatureFlag } from '@openfeature/react-sdk';
181
+
182
+ function App() {
183
+ return (
184
+ <OpenFeatureProvider>
185
+ {/* Basic usage - renders children when flag is truthy */}
186
+ <FeatureFlag flagKey="new-feature" defaultValue={false}>
187
+ <NewFeatureComponent />
188
+ </FeatureFlag>
189
+
190
+ {/* Match specific values */}
191
+ <FeatureFlag flagKey="theme" matchValue="dark" defaultValue="light">
192
+ <DarkThemeStyles />
193
+ </FeatureFlag>
194
+
195
+ {/* Boolean flag with fallback */}
196
+ <FeatureFlag flagKey="premium-feature" matchValue={true} defaultValue={false} fallback={<UpgradePrompt />}>
197
+ <PremiumContent />
198
+ </FeatureFlag>
199
+
200
+ {/* Custom predicate function for complex matching */}
201
+ <FeatureFlag
202
+ flagKey="user-segment"
203
+ defaultValue=""
204
+ matchValue="beta"
205
+ // check if the actual flag value includes the match ('beta')
206
+ predicate={(expected, actual) => !!expected && actual.value.includes(expected)}
207
+ >
208
+ <BetaFeatures />
209
+ </FeatureFlag>
210
+
211
+ {/* Function as children for accessing flag details */}
212
+ <FeatureFlag flagKey="experiment" defaultValue="control" matchValue="beta">
213
+ {({ value, reason }) => (
214
+ <span>
215
+ value is {value}, reason is {reason?.toString()}
216
+ </span>
217
+ )}
218
+ </FeatureFlag>
219
+ </OpenFeatureProvider>
220
+ );
221
+ }
172
222
  ```
173
223
 
224
+ The `FeatureFlag` component supports the following props:
225
+
226
+ - **`flagKey`** (required): The feature flag key to evaluate
227
+ - **`defaultValue`** (required): Default value when the flag is not available
228
+ - **`matchValue`** (required, except for boolean flags): Value to match against the flag value. By default, an optimized deep-comparison function is used.
229
+ - **`predicate`** (optional): Custom function for matching logic that receives the expected value and evaluation details
230
+ - **`children`**: Content to render when condition is met (can be JSX or a function receiving flag details)
231
+ - **`fallback`** (optional): Content to render when condition is not met
232
+
174
233
  #### Multiple Providers and Domains
175
234
 
176
235
  Multiple providers can be used by passing a `domain` to the `OpenFeatureProvider`:
@@ -313,8 +372,8 @@ The [OpenFeature debounce hook](https://github.com/open-feature/js-sdk-contrib/t
313
372
  ### Testing
314
373
 
315
374
  The React SDK includes a built-in context provider for testing.
316
- This allows you to easily test components that use evaluation hooks, such as `useFlag`.
317
- If you try to test a component (in this case, `MyComponent`) which uses an evaluation hook, you might see an error message like:
375
+ This allows you to easily test components that use evaluation hooks (such as `useFlag`) or declarative components (such as `FeatureFlag`).
376
+ If you try to test a component (in this case, `MyComponent`) which uses feature flags, you might see an error message like:
318
377
 
319
378
  > No OpenFeature client available - components using OpenFeature must be wrapped with an `<OpenFeatureProvider>`.
320
379
 
@@ -335,6 +394,16 @@ If you'd like to control the values returned by the evaluation hooks, you can pa
335
394
  <OpenFeatureTestProvider flagValueMap={{ 'my-boolean-flag': true }}>
336
395
  <MyComponent />
337
396
  </OpenFeatureTestProvider>
397
+
398
+ // testing declarative FeatureFlag components
399
+ <OpenFeatureTestProvider flagValueMap={{ 'new-feature': true, 'theme': 'dark' }}>
400
+ <FeatureFlag flagKey="new-feature" defaultValue={false}>
401
+ <NewFeature />
402
+ </FeatureFlag>
403
+ <FeatureFlag flagKey="theme" matchValue="dark" defaultValue="light">
404
+ <DarkMode />
405
+ </FeatureFlag>
406
+ </OpenFeatureTestProvider>
338
407
  ```
339
408
 
340
409
  Additionally, you can pass an artificial delay for the provider startup to test your suspense boundaries or loaders/spinners impacted by feature flags:
@@ -406,4 +475,4 @@ Avoid importing anything from `@openfeature/web-sdk` or `@openfeature/core`.
406
475
 
407
476
  ## Resources
408
477
 
409
- - [Example repo](https://github.com/open-feature/react-test-app)
478
+ - [Example repo](https://github.com/open-feature/react-test-app)
package/dist/cjs/index.js CHANGED
@@ -80,6 +80,7 @@ var __async = (__this, __arguments, generator) => {
80
80
  // src/index.ts
81
81
  var index_exports = {};
82
82
  __export(index_exports, {
83
+ FeatureFlag: () => FeatureFlag,
83
84
  OpenFeatureProvider: () => OpenFeatureProvider,
84
85
  OpenFeatureTestProvider: () => OpenFeatureTestProvider,
85
86
  useBooleanFlagDetails: () => useBooleanFlagDetails,
@@ -100,13 +101,18 @@ __export(index_exports, {
100
101
  });
101
102
  module.exports = __toCommonJS(index_exports);
102
103
 
104
+ // src/declarative/FeatureFlag.tsx
105
+ var import_react7 = __toESM(require("react"));
106
+
103
107
  // src/evaluation/use-feature-flag.ts
104
108
  var import_web_sdk5 = require("@openfeature/web-sdk");
105
109
  var import_react6 = require("react");
106
110
 
107
111
  // src/internal/context.ts
108
112
  var import_react = __toESM(require("react"));
109
- var Context = import_react.default.createContext(void 0);
113
+ var Context = import_react.default.createContext(
114
+ void 0
115
+ );
110
116
  function useProviderOptions() {
111
117
  const { options } = import_react.default.useContext(Context) || {};
112
118
  return normalizeOptions(options);
@@ -266,7 +272,7 @@ function useOpenFeatureProvider() {
266
272
  if (!openFeatureContext) {
267
273
  throw new MissingContextError("No OpenFeature context available");
268
274
  }
269
- return import_web_sdk3.OpenFeature.getProvider(openFeatureContext.domain);
275
+ return import_web_sdk3.OpenFeature.getProvider(openFeatureContext.client.metadata.domain);
270
276
  }
271
277
 
272
278
  // src/internal/hook-flag-query.ts
@@ -419,9 +425,7 @@ function attachHandlersAndResolve(flagKey, defaultValue, resolver, options) {
419
425
  );
420
426
  (0, import_react6.useEffect)(() => {
421
427
  const controller = new AbortController();
422
- if (status === import_web_sdk5.ProviderStatus.NOT_READY) {
423
- client.addHandler(import_web_sdk5.ProviderEvents.Ready, updateEvaluationDetailsCallback, { signal: controller.signal });
424
- }
428
+ client.addHandler(import_web_sdk5.ProviderEvents.Ready, updateEvaluationDetailsCallback, { signal: controller.signal });
425
429
  if (defaultedOptions.updateOnContextChanged) {
426
430
  client.addHandler(import_web_sdk5.ProviderEvents.ContextChanged, updateEvaluationDetailsCallback, { signal: controller.signal });
427
431
  }
@@ -435,7 +439,6 @@ function attachHandlersAndResolve(flagKey, defaultValue, resolver, options) {
435
439
  };
436
440
  }, [
437
441
  client,
438
- status,
439
442
  defaultedOptions.updateOnContextChanged,
440
443
  defaultedOptions.updateOnConfigurationChanged,
441
444
  updateEvaluationDetailsCallback,
@@ -444,13 +447,51 @@ function attachHandlersAndResolve(flagKey, defaultValue, resolver, options) {
444
447
  return evaluationDetails;
445
448
  }
446
449
 
450
+ // src/declarative/FeatureFlag.tsx
451
+ function equals(expected, actual) {
452
+ return isEqual(expected, actual.value);
453
+ }
454
+ function FeatureFlag({
455
+ flagKey,
456
+ matchValue,
457
+ predicate,
458
+ defaultValue,
459
+ children,
460
+ evaluationOptions = {},
461
+ fallback = null
462
+ }) {
463
+ const details = useFlag(flagKey, defaultValue, __spreadValues({
464
+ updateOnContextChanged: true
465
+ }, evaluationOptions));
466
+ if (details.reason === "ERROR") {
467
+ const fallbackNode2 = typeof fallback === "function" ? fallback(details.details) : fallback;
468
+ return /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, fallbackNode2);
469
+ }
470
+ let shouldRender = false;
471
+ if (predicate) {
472
+ shouldRender = predicate(matchValue, details.details);
473
+ } else if (matchValue !== void 0) {
474
+ shouldRender = equals(matchValue, details.details);
475
+ } else if (details.type === "boolean") {
476
+ shouldRender = Boolean(details.value);
477
+ } else {
478
+ shouldRender = false;
479
+ }
480
+ if (shouldRender) {
481
+ const childNode = typeof children === "function" ? children(details) : children;
482
+ return /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, childNode);
483
+ }
484
+ const fallbackNode = typeof fallback === "function" ? fallback(details.details) : fallback;
485
+ return /* @__PURE__ */ import_react7.default.createElement(import_react7.default.Fragment, null, fallbackNode);
486
+ }
487
+
447
488
  // src/provider/provider.tsx
448
489
  var import_web_sdk6 = require("@openfeature/web-sdk");
449
- var React5 = __toESM(require("react"));
490
+ var React6 = __toESM(require("react"));
450
491
  function OpenFeatureProvider(_a) {
451
492
  var _b = _a, { client, domain, children } = _b, options = __objRest(_b, ["client", "domain", "children"]);
452
- const stableClient = React5.useMemo(() => client || import_web_sdk6.OpenFeature.getClient(domain), [client, domain]);
453
- return /* @__PURE__ */ React5.createElement(Context.Provider, { value: { client: stableClient, options, domain } }, children);
493
+ const stableClient = React6.useMemo(() => client || import_web_sdk6.OpenFeature.getClient(domain), [client, domain]);
494
+ return /* @__PURE__ */ React6.createElement(Context.Provider, { value: { client: stableClient, options } }, children);
454
495
  }
455
496
 
456
497
  // src/provider/use-when-provider-ready.ts
@@ -468,7 +509,7 @@ function useWhenProviderReady(options) {
468
509
 
469
510
  // src/provider/test-provider.tsx
470
511
  var import_web_sdk8 = require("@openfeature/web-sdk");
471
- var import_react7 = __toESM(require("react"));
512
+ var import_react8 = __toESM(require("react"));
472
513
  var TEST_VARIANT = "test-variant";
473
514
  var TEST_PROVIDER = "test-provider";
474
515
  var TestProvider = class extends import_web_sdk8.InMemoryProvider {
@@ -508,10 +549,12 @@ function OpenFeatureTestProvider(testProviderOptions) {
508
549
  const { flagValueMap, provider } = testProviderOptions;
509
550
  const effectiveProvider = flagValueMap ? new TestProvider(flagValueMap, testProviderOptions.delayMs) : mixInNoop(provider) || import_web_sdk8.NOOP_PROVIDER;
510
551
  testProviderOptions.domain ? import_web_sdk8.OpenFeature.setProvider(testProviderOptions.domain, effectiveProvider) : import_web_sdk8.OpenFeature.setProvider(effectiveProvider);
511
- return /* @__PURE__ */ import_react7.default.createElement(OpenFeatureProvider, __spreadProps(__spreadValues({}, testProviderOptions), { domain: testProviderOptions.domain }), testProviderOptions.children);
552
+ return /* @__PURE__ */ import_react8.default.createElement(OpenFeatureProvider, __spreadProps(__spreadValues({}, testProviderOptions), { domain: testProviderOptions.domain }), testProviderOptions.children);
512
553
  }
513
554
  function mixInNoop(provider = {}) {
514
- for (const prop of Object.getOwnPropertyNames(Object.getPrototypeOf(import_web_sdk8.NOOP_PROVIDER)).filter((prop2) => prop2 !== "constructor")) {
555
+ for (const prop of Object.getOwnPropertyNames(Object.getPrototypeOf(import_web_sdk8.NOOP_PROVIDER)).filter(
556
+ (prop2) => prop2 !== "constructor"
557
+ )) {
515
558
  const patchedProvider = provider;
516
559
  if (!Object.getPrototypeOf(patchedProvider)[prop] && !patchedProvider[prop]) {
517
560
  patchedProvider[prop] = Object.getPrototypeOf(import_web_sdk8.NOOP_PROVIDER)[prop];
@@ -524,31 +567,50 @@ function mixInNoop(provider = {}) {
524
567
  }
525
568
 
526
569
  // src/context/use-context-mutator.ts
527
- var import_react8 = require("react");
570
+ var import_react9 = require("react");
528
571
  var import_web_sdk9 = require("@openfeature/web-sdk");
529
572
  function useContextMutator(options = { defaultContext: false }) {
530
- const { domain } = (0, import_react8.useContext)(Context) || {};
531
- const setContext = (0, import_react8.useCallback)((updatedContext) => __async(null, null, function* () {
532
- const previousContext = import_web_sdk9.OpenFeature.getContext((options == null ? void 0 : options.defaultContext) ? void 0 : domain);
533
- const resolvedContext = typeof updatedContext === "function" ? updatedContext(previousContext) : updatedContext;
534
- if (previousContext !== resolvedContext) {
535
- if (!domain || (options == null ? void 0 : options.defaultContext)) {
536
- yield import_web_sdk9.OpenFeature.setContext(resolvedContext);
537
- } else {
538
- yield import_web_sdk9.OpenFeature.setContext(domain, resolvedContext);
573
+ const { client } = (0, import_react9.useContext)(Context) || {};
574
+ const domain = client == null ? void 0 : client.metadata.domain;
575
+ const [warned, setWarned] = (0, import_react9.useState)(false);
576
+ (0, import_react9.useEffect)(() => {
577
+ if (options.defaultContext || domain) {
578
+ if (warned) {
579
+ setWarned(false);
539
580
  }
581
+ return;
582
+ }
583
+ if (!warned) {
584
+ console.warn(
585
+ "[useContextMutator] No domain available from OpenFeature context; are you using <OpenFeatureProvider/>? setContext will mutate the default context, as if `defaultContext: true` were set. This may result in a thrown error in the future."
586
+ );
587
+ setWarned(true);
540
588
  }
541
- }), [domain, options == null ? void 0 : options.defaultContext]);
589
+ }, [warned]);
590
+ const setContext = (0, import_react9.useCallback)(
591
+ (updatedContext) => __async(null, null, function* () {
592
+ const previousContext = import_web_sdk9.OpenFeature.getContext((options == null ? void 0 : options.defaultContext) ? void 0 : domain);
593
+ const resolvedContext = typeof updatedContext === "function" ? updatedContext(previousContext) : updatedContext;
594
+ if (previousContext !== resolvedContext) {
595
+ if (!domain || (options == null ? void 0 : options.defaultContext)) {
596
+ yield import_web_sdk9.OpenFeature.setContext(resolvedContext);
597
+ } else {
598
+ yield import_web_sdk9.OpenFeature.setContext(domain, resolvedContext);
599
+ }
600
+ }
601
+ }),
602
+ [domain, options == null ? void 0 : options.defaultContext]
603
+ );
542
604
  return {
543
605
  setContext
544
606
  };
545
607
  }
546
608
 
547
609
  // src/tracking/use-track.ts
548
- var import_react9 = require("react");
610
+ var import_react10 = require("react");
549
611
  function useTrack() {
550
612
  const client = useOpenFeatureClient();
551
- const track = (0, import_react9.useCallback)((trackingEventName, trackingEventDetails) => {
613
+ const track = (0, import_react10.useCallback)((trackingEventName, trackingEventDetails) => {
552
614
  client.track(trackingEventName, trackingEventDetails);
553
615
  }, []);
554
616
  return {