@featureflip/react 1.0.0

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 ADDED
@@ -0,0 +1,112 @@
1
+ # @featureflip/react-sdk
2
+
3
+ React bindings for Featureflip feature flag evaluation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @featureflip/react-sdk @featureflip/browser-sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ Wrap your app with `FeatureflipProvider`, then use `useFeatureFlag` in any component.
14
+
15
+ ```tsx
16
+ import { FeatureflipProvider, useFeatureFlag } from '@featureflip/react-sdk';
17
+
18
+ function App() {
19
+ return (
20
+ <FeatureflipProvider clientKey="your-client-sdk-key">
21
+ <Banner />
22
+ </FeatureflipProvider>
23
+ );
24
+ }
25
+
26
+ function Banner() {
27
+ const showBanner = useFeatureFlag('show-banner', false);
28
+ if (!showBanner) return null;
29
+ return <div>New feature available!</div>;
30
+ }
31
+ ```
32
+
33
+ ## Provider Props
34
+
35
+ | Prop | Type | Default | Description |
36
+ |---|---|---|---|
37
+ | `clientKey` | `string` | **(required)** | Client SDK key |
38
+ | `context` | `Record<string, unknown>` | `undefined` | Evaluation context (user attributes) |
39
+ | `baseUrl` | `string` | `undefined` | Evaluation API base URL |
40
+ | `streaming` | `boolean` | `undefined` | Enable SSE streaming |
41
+
42
+ The provider creates a `FeatureflipClient` internally and calls `initialize()` on mount. It cleans up (calls `close()`) on unmount.
43
+
44
+ ## Hooks
45
+
46
+ ### `useFeatureFlag<T>(key: string, defaultValue: T): T`
47
+
48
+ Returns the evaluated value of a flag. The component re-renders automatically when the flag value changes via streaming or `identify()`. The type of `defaultValue` determines which variation method is called (`boolVariation`, `stringVariation`, `numberVariation`, or `jsonVariation`).
49
+
50
+ ```tsx
51
+ const enabled = useFeatureFlag('new-checkout', false);
52
+ const color = useFeatureFlag('button-color', 'blue');
53
+ const limit = useFeatureFlag('rate-limit', 100);
54
+ const config = useFeatureFlag('ui-config', { sidebar: true });
55
+ ```
56
+
57
+ ### `useFeatureflipStatus(): { isReady, isError, error }`
58
+
59
+ Returns the initialization status of the client. Useful for showing loading states.
60
+
61
+ ```tsx
62
+ function App() {
63
+ const { isReady, isError, error } = useFeatureflipStatus();
64
+
65
+ if (isError) return <div>Error: {error?.message}</div>;
66
+ if (!isReady) return <div>Loading...</div>;
67
+
68
+ return <Main />;
69
+ }
70
+ ```
71
+
72
+ ### `useFeatureflipClient(): FeatureflipClient`
73
+
74
+ Returns the underlying `FeatureflipClient` instance for direct access (e.g., calling `identify`).
75
+
76
+ ## Identify on Login
77
+
78
+ When the context prop changes, the provider automatically calls `identify()` and re-evaluates all flags. You can also call `identify` directly.
79
+
80
+ ```tsx
81
+ function LoginPage() {
82
+ const client = useFeatureflipClient();
83
+
84
+ async function handleLogin(userId: string) {
85
+ // ... authenticate ...
86
+ await client.identify({ user_id: userId, plan: 'pro' });
87
+ }
88
+
89
+ return <button onClick={() => handleLogin('123')}>Log in</button>;
90
+ }
91
+ ```
92
+
93
+ ## Testing
94
+
95
+ Use `TestFeatureflipProvider` to supply predetermined flag values in tests -- no network calls, no initialization delay.
96
+
97
+ ```tsx
98
+ import { TestFeatureflipProvider } from '@featureflip/react-sdk';
99
+ import { render, screen } from '@testing-library/react';
100
+
101
+ test('renders banner when flag is on', () => {
102
+ render(
103
+ <TestFeatureflipProvider flags={{ 'show-banner': true }}>
104
+ <Banner />
105
+ </TestFeatureflipProvider>,
106
+ );
107
+
108
+ expect(screen.getByText('New feature available!')).toBeInTheDocument();
109
+ });
110
+ ```
111
+
112
+ `TestFeatureflipProvider` uses `FeatureflipClient.forTesting()` internally and sets `isReady: true` immediately.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hooks.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/hooks.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=provider.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/provider.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,9 @@
1
+ import { FeatureflipClient } from '../../browser-sdk/src';
2
+ export interface FeatureflipContextValue {
3
+ client: FeatureflipClient;
4
+ isReady: boolean;
5
+ isError: boolean;
6
+ error: Error | null;
7
+ }
8
+ export declare const FeatureflipContext: import('react').Context<FeatureflipContextValue | null>;
9
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED,eAAO,MAAM,kBAAkB,yDAAsD,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { FeatureflipClient } from '../../browser-sdk/src';
2
+ export declare function useFeatureFlag<T>(key: string, defaultValue: T): T;
3
+ export declare function useFeatureflipClient(): FeatureflipClient;
4
+ export declare function useFeatureflipStatus(): {
5
+ isReady: boolean;
6
+ isError: boolean;
7
+ error: Error | null;
8
+ };
9
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AA0B9D,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,GAAG,CAAC,CAmDjE;AAED,wBAAgB,oBAAoB,IAAI,iBAAiB,CAGxD;AAED,wBAAgB,oBAAoB,IAAI;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB,CAGA"}
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./testing-CXh0WG3c.cjs`);let t=require(`react`),n=require(`@featureflip/browser`),r=require(`react/jsx-runtime`);function i({clientKey:i,context:a,baseUrl:o,streaming:s,children:c}){let l=(0,t.useRef)(null),[u,d]=(0,t.useState)(0),[f,p]=(0,t.useState)(!1),[m,h]=(0,t.useState)(!1),[g,_]=(0,t.useState)(null),v=(0,t.useRef)(``);l.current===null&&(l.current=new n.FeatureflipClient({clientKey:i,baseUrl:o,context:a,streaming:s})),(0,t.useEffect)(()=>{l.current||(l.current=new n.FeatureflipClient({clientKey:i,baseUrl:o,context:a,streaming:s}),p(!1),h(!1),_(null),d(e=>e+1));let e=l.current,t=()=>p(!0),r=e=>{h(!0),_(e instanceof Error?e:Error(String(e)))};return e.on(`ready`,t),e.on(`error`,r),e.initialize().catch(r),()=>{e.off(`ready`,t),e.off(`error`,r),e.close(),l.current=null}},[]),(0,t.useEffect)(()=>{let e=JSON.stringify(a??{});if(v.current===``){v.current=e;return}e!==v.current&&(v.current=e,l.current?.identify(a??{}).catch(()=>{}))},[a]);let y={client:l.current,isReady:f,isError:m,error:g};return(0,r.jsx)(e.n.Provider,{value:y,children:c})}function a(){let n=(0,t.useContext)(e.n);if(n===null)throw Error(`useFeatureFlag must be used within a <FeatureflipProvider>`);return n}function o(e,t,n){switch(typeof n){case`boolean`:return e.boolVariation(t,n);case`string`:return e.stringVariation(t,n);case`number`:return e.numberVariation(t,n);default:return e.jsonVariation(t,n)}}function s(e,n){let{client:r}=a(),i=(0,t.useRef)(n);i.current=n;let s=(0,t.useRef)(void 0),c=(0,t.useRef)(void 0),l=(0,t.useCallback)(e=>{let t=()=>e();return r.on(`change`,t),()=>r.off(`change`,t)},[r]),u=(0,t.useCallback)(()=>{let t=o(r,e,i.current);if(Object.is(t,s.current))return s.current;if(typeof t!=`object`||!t)return s.current=t,t;try{let e=JSON.stringify(t);if(s.current!==void 0&&e===c.current)return s.current;s.current=t,c.current=e}catch{s.current=t}return t},[r,e]);return(0,t.useSyncExternalStore)(l,u,u)}function c(){let{client:e}=a();return e}function l(){let{isReady:e,isError:t,error:n}=a();return{isReady:e,isError:t,error:n}}exports.FeatureflipContext=e.n,exports.FeatureflipProvider=i,exports.TestFeatureflipProvider=e.t,exports.useFeatureFlag=s,exports.useFeatureflipClient=c,exports.useFeatureflipStatus=l;
@@ -0,0 +1,7 @@
1
+ export { FeatureflipProvider } from './provider';
2
+ export type { FeatureflipProviderProps } from './provider';
3
+ export { useFeatureFlag, useFeatureflipClient, useFeatureflipStatus } from './hooks';
4
+ export { TestFeatureflipProvider } from './testing';
5
+ export { FeatureflipContext } from './context';
6
+ export type { FeatureflipContextValue } from './context';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,YAAY,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,YAAY,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,94 @@
1
+ import { n as e, t } from "./testing-D-nDtDyw.js";
2
+ import { useCallback as n, useContext as r, useEffect as i, useRef as a, useState as o, useSyncExternalStore as s } from "react";
3
+ import { FeatureflipClient as c } from "@featureflip/browser";
4
+ import { jsx as l } from "react/jsx-runtime";
5
+ //#region src/provider.tsx
6
+ function u({ clientKey: t, context: n, baseUrl: r, streaming: s, children: u }) {
7
+ let d = a(null), [f, p] = o(0), [m, h] = o(!1), [g, _] = o(!1), [v, y] = o(null), b = a("");
8
+ d.current === null && (d.current = new c({
9
+ clientKey: t,
10
+ baseUrl: r,
11
+ context: n,
12
+ streaming: s
13
+ })), i(() => {
14
+ d.current || (d.current = new c({
15
+ clientKey: t,
16
+ baseUrl: r,
17
+ context: n,
18
+ streaming: s
19
+ }), h(!1), _(!1), y(null), p((e) => e + 1));
20
+ let e = d.current, i = () => h(!0), a = (e) => {
21
+ _(!0), y(e instanceof Error ? e : Error(String(e)));
22
+ };
23
+ return e.on("ready", i), e.on("error", a), e.initialize().catch(a), () => {
24
+ e.off("ready", i), e.off("error", a), e.close(), d.current = null;
25
+ };
26
+ }, []), i(() => {
27
+ let e = JSON.stringify(n ?? {});
28
+ if (b.current === "") {
29
+ b.current = e;
30
+ return;
31
+ }
32
+ e !== b.current && (b.current = e, d.current?.identify(n ?? {}).catch(() => {}));
33
+ }, [n]);
34
+ let x = {
35
+ client: d.current,
36
+ isReady: m,
37
+ isError: g,
38
+ error: v
39
+ };
40
+ return /* @__PURE__ */ l(e.Provider, {
41
+ value: x,
42
+ children: u
43
+ });
44
+ }
45
+ //#endregion
46
+ //#region src/hooks.ts
47
+ function d() {
48
+ let t = r(e);
49
+ if (t === null) throw Error("useFeatureFlag must be used within a <FeatureflipProvider>");
50
+ return t;
51
+ }
52
+ function f(e, t, n) {
53
+ switch (typeof n) {
54
+ case "boolean": return e.boolVariation(t, n);
55
+ case "string": return e.stringVariation(t, n);
56
+ case "number": return e.numberVariation(t, n);
57
+ default: return e.jsonVariation(t, n);
58
+ }
59
+ }
60
+ function p(e, t) {
61
+ let { client: r } = d(), i = a(t);
62
+ i.current = t;
63
+ let o = a(void 0), c = a(void 0), l = n((e) => {
64
+ let t = () => e();
65
+ return r.on("change", t), () => r.off("change", t);
66
+ }, [r]), u = n(() => {
67
+ let t = f(r, e, i.current);
68
+ if (Object.is(t, o.current)) return o.current;
69
+ if (typeof t != "object" || !t) return o.current = t, t;
70
+ try {
71
+ let e = JSON.stringify(t);
72
+ if (o.current !== void 0 && e === c.current) return o.current;
73
+ o.current = t, c.current = e;
74
+ } catch {
75
+ o.current = t;
76
+ }
77
+ return t;
78
+ }, [r, e]);
79
+ return s(l, u, u);
80
+ }
81
+ function m() {
82
+ let { client: e } = d();
83
+ return e;
84
+ }
85
+ function h() {
86
+ let { isReady: e, isError: t, error: n } = d();
87
+ return {
88
+ isReady: e,
89
+ isError: t,
90
+ error: n
91
+ };
92
+ }
93
+ //#endregion
94
+ export { e as FeatureflipContext, u as FeatureflipProvider, t as TestFeatureflipProvider, p as useFeatureFlag, m as useFeatureflipClient, h as useFeatureflipStatus };
@@ -0,0 +1,10 @@
1
+ import { ReactNode } from 'react';
2
+ export interface FeatureflipProviderProps {
3
+ clientKey: string;
4
+ context?: Record<string, unknown>;
5
+ baseUrl?: string;
6
+ streaming?: boolean;
7
+ children: ReactNode;
8
+ }
9
+ export declare function FeatureflipProvider({ clientKey, context, baseUrl, streaming, children, }: FeatureflipProviderProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAIpE,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,GACT,EAAE,wBAAwB,2CAmF1B"}
@@ -0,0 +1 @@
1
+ let e=require(`react`),t=require(`@featureflip/browser`),n=require(`react/jsx-runtime`);var r=(0,e.createContext)(null);function i({flags:e,children:i}){let a=t.FeatureflipClient.forTesting(e);return(0,n.jsx)(r.Provider,{value:{client:a,isReady:!0,isError:!1,error:null},children:i})}Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return i}});
@@ -0,0 +1,21 @@
1
+ import { createContext as e } from "react";
2
+ import { FeatureflipClient as t } from "@featureflip/browser";
3
+ import { jsx as n } from "react/jsx-runtime";
4
+ //#region src/context.ts
5
+ var r = e(null);
6
+ //#endregion
7
+ //#region src/testing.tsx
8
+ function i({ flags: e, children: i }) {
9
+ let a = t.forTesting(e);
10
+ return /* @__PURE__ */ n(r.Provider, {
11
+ value: {
12
+ client: a,
13
+ isReady: !0,
14
+ isError: !1,
15
+ error: null
16
+ },
17
+ children: i
18
+ });
19
+ }
20
+ //#endregion
21
+ export { r as n, i as t };
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./testing-CXh0WG3c.cjs`);exports.TestFeatureflipProvider=e.t;
@@ -0,0 +1,8 @@
1
+ import { ReactNode } from 'react';
2
+ interface TestFeatureflipProviderProps {
3
+ flags: Record<string, unknown>;
4
+ children: ReactNode;
5
+ }
6
+ export declare function TestFeatureflipProvider({ flags, children }: TestFeatureflipProviderProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,UAAU,4BAA4B;IACpC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,uBAAuB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,4BAA4B,2CAOxF"}
@@ -0,0 +1,2 @@
1
+ import { t as e } from "./testing-D-nDtDyw.js";
2
+ export { e as TestFeatureflipProvider };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@featureflip/react",
3
+ "version": "1.0.0",
4
+ "description": "React SDK for Featureflip - React bindings for feature flag evaluation",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./testing": {
16
+ "types": "./dist/testing.d.ts",
17
+ "import": "./dist/testing.js",
18
+ "require": "./dist/testing.cjs"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "vite build",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest"
28
+ },
29
+ "license": "Apache-2.0",
30
+ "homepage": "https://featureflip.io/docs",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "peerDependencies": {
35
+ "@featureflip/browser": "^1.0.0",
36
+ "react": "^18.0.0 || ^19.0.0"
37
+ },
38
+ "engines": {
39
+ "node": ">=20.19.0"
40
+ },
41
+ "devDependencies": {
42
+ "@featureflip/browser": "file:../browser-sdk",
43
+ "@testing-library/react": "^16.0.0",
44
+ "@types/react": "^19.1.0",
45
+ "@types/react-dom": "^19.1.0",
46
+ "jsdom": "^28.0.0",
47
+ "react": "^19.1.0",
48
+ "react-dom": "^19.1.0",
49
+ "typescript": "^5.9.0",
50
+ "vite": "^8.0.0",
51
+ "vite-plugin-dts": "^4.0.0",
52
+ "vitest": "^4.1.0"
53
+ }
54
+ }