featuretoggle-sdk-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,43 @@
1
+ # featuretoggle-sdk-react
2
+
3
+ React adapter for [featuretoggle-sdk-typescript](https://www.npmjs.com/package/featuretoggle-sdk-typescript) — `FeatureToggleProvider`, `useFeature`, and `useFeatureToggle`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add featuretoggle-sdk-react featuretoggle-sdk-typescript react
9
+ ```
10
+
11
+ Peers: `react` (≥18), `featuretoggle-sdk-typescript` (^1.0.0).
12
+
13
+ ## Quick start
14
+
15
+ ```tsx
16
+ import { FeatureToggleProvider, useFeature } from "featuretoggle-sdk-react";
17
+
18
+ function App() {
19
+ return (
20
+ <FeatureToggleProvider apiKey={import.meta.env.VITE_FT_API_KEY}>
21
+ <Checkout />
22
+ </FeatureToggleProvider>
23
+ );
24
+ }
25
+
26
+ function Checkout() {
27
+ const { enabled, loading } = useFeature("new-checkout");
28
+ if (loading) return null;
29
+ return enabled ? <NewCheckout /> : <LegacyCheckout />;
30
+ }
31
+ ```
32
+
33
+ ## Integration patterns
34
+
35
+ See the [integration cookbook](https://github.com/feature-toggle/feature-toggle-monorepo/blob/main/docs/17-sdk-integration-patterns.md) for SSR seed, BYO client, `autoInit={false}`, and more (patterns I–N).
36
+
37
+ ## API
38
+
39
+ - `<FeatureToggleProvider>` — `apiKey`, optional `client`, `initialFeatures`, `initialEtag`, `autoInit` (default `true`)
40
+ - `useFeature(key)` — `{ enabled, value, loading, error }`
41
+ - `useFeatureToggle()` — `{ init, refresh, getFeatures, loading, error }`
42
+
43
+ Import `FeatureResponse` from `featuretoggle-sdk-typescript` — not re-exported from this package.
package/dist/index.cjs ADDED
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ FeatureToggleProvider: () => FeatureToggleProvider,
24
+ useFeature: () => useFeature,
25
+ useFeatureToggle: () => useFeatureToggle
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/provider.tsx
30
+ var import_featuretoggle_sdk_typescript = require("featuretoggle-sdk-typescript");
31
+ var import_react2 = require("react");
32
+
33
+ // src/context.ts
34
+ var import_react = require("react");
35
+ var FeatureToggleContext = (0, import_react.createContext)(
36
+ null
37
+ );
38
+
39
+ // src/provider.tsx
40
+ var import_jsx_runtime = require("react/jsx-runtime");
41
+ function useFeatureToggleContext() {
42
+ const value = (0, import_react2.useContext)(FeatureToggleContext);
43
+ if (!value) {
44
+ throw new Error("useFeature must be used within FeatureToggleProvider");
45
+ }
46
+ return value;
47
+ }
48
+ function isSeeded(initialFeatures) {
49
+ return initialFeatures !== void 0;
50
+ }
51
+ function FeatureToggleProvider({
52
+ apiKey,
53
+ client: clientProp,
54
+ initialFeatures,
55
+ initialEtag,
56
+ autoInit = true,
57
+ pollInterval: _pollInterval,
58
+ children
59
+ }) {
60
+ const ownsClient = clientProp === void 0;
61
+ const seeded = isSeeded(initialFeatures);
62
+ const initGenerationRef = (0, import_react2.useRef)(0);
63
+ const clientRef = (0, import_react2.useRef)(null);
64
+ const apiKeyRef = (0, import_react2.useRef)(apiKey);
65
+ if (clientProp) {
66
+ clientRef.current = null;
67
+ } else if (apiKey) {
68
+ if (!clientRef.current || apiKeyRef.current !== apiKey) {
69
+ clientRef.current?.close();
70
+ clientRef.current = new import_featuretoggle_sdk_typescript.FeatureToggle({
71
+ apiKey,
72
+ initialFeatures,
73
+ initialEtag
74
+ });
75
+ apiKeyRef.current = apiKey;
76
+ }
77
+ } else {
78
+ throw new Error("FeatureToggleProvider requires apiKey or client");
79
+ }
80
+ const client = clientProp ?? clientRef.current;
81
+ const [loading, setLoading] = (0, import_react2.useState)(() => !seeded);
82
+ const [error, setError] = (0, import_react2.useState)(null);
83
+ const runInit = (0, import_react2.useCallback)(async () => {
84
+ const generation = ++initGenerationRef.current;
85
+ setError(null);
86
+ if (!seeded) {
87
+ setLoading(true);
88
+ }
89
+ try {
90
+ await client.init();
91
+ if (initGenerationRef.current !== generation) return;
92
+ setLoading(false);
93
+ } catch (unknownError) {
94
+ if (initGenerationRef.current !== generation) return;
95
+ const nextError = unknownError instanceof Error ? unknownError : new Error(String(unknownError));
96
+ setError(nextError);
97
+ setLoading(false);
98
+ throw nextError;
99
+ }
100
+ }, [client, seeded]);
101
+ const refresh = (0, import_react2.useCallback)(async () => {
102
+ await client.refresh();
103
+ }, [client]);
104
+ (0, import_react2.useEffect)(() => {
105
+ if (!autoInit) return;
106
+ void runInit().catch(() => void 0);
107
+ }, [autoInit, runInit]);
108
+ (0, import_react2.useEffect)(() => {
109
+ if (!ownsClient) return;
110
+ return () => {
111
+ initGenerationRef.current += 1;
112
+ clientRef.current?.close();
113
+ clientRef.current = null;
114
+ };
115
+ }, [ownsClient]);
116
+ const contextValue = (0, import_react2.useMemo)(
117
+ () => ({
118
+ client,
119
+ loading,
120
+ error,
121
+ init: runInit,
122
+ refresh
123
+ }),
124
+ [client, loading, error, runInit, refresh]
125
+ );
126
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FeatureToggleContext.Provider, { value: contextValue, children });
127
+ }
128
+ function useFeature(key) {
129
+ const { client, loading, error } = useFeatureToggleContext();
130
+ const snapshot = (0, import_react2.useSyncExternalStore)(
131
+ (listener) => client.subscribe(listener),
132
+ () => JSON.stringify({
133
+ enabled: client.isEnabled(key),
134
+ value: client.getValue(key)
135
+ }),
136
+ () => JSON.stringify({ enabled: false, value: void 0 })
137
+ );
138
+ const parsed = JSON.parse(snapshot);
139
+ return {
140
+ enabled: parsed.enabled,
141
+ value: parsed.value,
142
+ loading,
143
+ error
144
+ };
145
+ }
146
+ function useFeatureToggle() {
147
+ const { client, loading, error, init, refresh } = useFeatureToggleContext();
148
+ (0, import_react2.useSyncExternalStore)(
149
+ (listener) => client.subscribe(listener),
150
+ () => JSON.stringify(client.getFeatures()),
151
+ () => "[]"
152
+ );
153
+ return {
154
+ init,
155
+ refresh,
156
+ getFeatures: (options) => client.getFeatures(options),
157
+ loading,
158
+ error
159
+ };
160
+ }
161
+ // Annotate the CommonJS export names for ESM import in node:
162
+ 0 && (module.exports = {
163
+ FeatureToggleProvider,
164
+ useFeature,
165
+ useFeatureToggle
166
+ });
@@ -0,0 +1,38 @@
1
+ import * as react from 'react';
2
+ import { FeatureToggle, FeatureResponse } from 'featuretoggle-sdk-typescript';
3
+
4
+ type FeatureToggleProviderProps = {
5
+ apiKey?: string;
6
+ client?: FeatureToggle;
7
+ pollInterval?: number;
8
+ initialFeatures?: FeatureResponse[];
9
+ initialEtag?: string;
10
+ autoInit?: boolean;
11
+ children: React.ReactNode;
12
+ };
13
+ type UseFeatureResult<T = unknown> = {
14
+ enabled: boolean;
15
+ value: T | undefined;
16
+ loading: boolean;
17
+ error: Error | null;
18
+ };
19
+ type UseFeatureToggleResult = {
20
+ init: () => Promise<void>;
21
+ refresh: () => Promise<void>;
22
+ getFeatures: (options?: Parameters<FeatureToggle["getFeatures"]>[0]) => FeatureResponse[];
23
+ loading: boolean;
24
+ error: Error | null;
25
+ };
26
+ type FeatureToggleContextValue = {
27
+ client: FeatureToggle;
28
+ loading: boolean;
29
+ error: Error | null;
30
+ init: () => Promise<void>;
31
+ refresh: () => Promise<void>;
32
+ };
33
+
34
+ declare function FeatureToggleProvider({ apiKey, client: clientProp, initialFeatures, initialEtag, autoInit, pollInterval: _pollInterval, children, }: FeatureToggleProviderProps): react.JSX.Element;
35
+ declare function useFeature<T = unknown>(key: string): UseFeatureResult<T>;
36
+ declare function useFeatureToggle(): UseFeatureToggleResult;
37
+
38
+ export { type FeatureToggleContextValue, FeatureToggleProvider, type FeatureToggleProviderProps, type UseFeatureResult, type UseFeatureToggleResult, useFeature, useFeatureToggle };
@@ -0,0 +1,38 @@
1
+ import * as react from 'react';
2
+ import { FeatureToggle, FeatureResponse } from 'featuretoggle-sdk-typescript';
3
+
4
+ type FeatureToggleProviderProps = {
5
+ apiKey?: string;
6
+ client?: FeatureToggle;
7
+ pollInterval?: number;
8
+ initialFeatures?: FeatureResponse[];
9
+ initialEtag?: string;
10
+ autoInit?: boolean;
11
+ children: React.ReactNode;
12
+ };
13
+ type UseFeatureResult<T = unknown> = {
14
+ enabled: boolean;
15
+ value: T | undefined;
16
+ loading: boolean;
17
+ error: Error | null;
18
+ };
19
+ type UseFeatureToggleResult = {
20
+ init: () => Promise<void>;
21
+ refresh: () => Promise<void>;
22
+ getFeatures: (options?: Parameters<FeatureToggle["getFeatures"]>[0]) => FeatureResponse[];
23
+ loading: boolean;
24
+ error: Error | null;
25
+ };
26
+ type FeatureToggleContextValue = {
27
+ client: FeatureToggle;
28
+ loading: boolean;
29
+ error: Error | null;
30
+ init: () => Promise<void>;
31
+ refresh: () => Promise<void>;
32
+ };
33
+
34
+ declare function FeatureToggleProvider({ apiKey, client: clientProp, initialFeatures, initialEtag, autoInit, pollInterval: _pollInterval, children, }: FeatureToggleProviderProps): react.JSX.Element;
35
+ declare function useFeature<T = unknown>(key: string): UseFeatureResult<T>;
36
+ declare function useFeatureToggle(): UseFeatureToggleResult;
37
+
38
+ export { type FeatureToggleContextValue, FeatureToggleProvider, type FeatureToggleProviderProps, type UseFeatureResult, type UseFeatureToggleResult, useFeature, useFeatureToggle };
package/dist/index.js ADDED
@@ -0,0 +1,147 @@
1
+ // src/provider.tsx
2
+ import {
3
+ FeatureToggle
4
+ } from "featuretoggle-sdk-typescript";
5
+ import {
6
+ useCallback,
7
+ useContext,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ useSyncExternalStore
13
+ } from "react";
14
+
15
+ // src/context.ts
16
+ import { createContext } from "react";
17
+ var FeatureToggleContext = createContext(
18
+ null
19
+ );
20
+
21
+ // src/provider.tsx
22
+ import { jsx } from "react/jsx-runtime";
23
+ function useFeatureToggleContext() {
24
+ const value = useContext(FeatureToggleContext);
25
+ if (!value) {
26
+ throw new Error("useFeature must be used within FeatureToggleProvider");
27
+ }
28
+ return value;
29
+ }
30
+ function isSeeded(initialFeatures) {
31
+ return initialFeatures !== void 0;
32
+ }
33
+ function FeatureToggleProvider({
34
+ apiKey,
35
+ client: clientProp,
36
+ initialFeatures,
37
+ initialEtag,
38
+ autoInit = true,
39
+ pollInterval: _pollInterval,
40
+ children
41
+ }) {
42
+ const ownsClient = clientProp === void 0;
43
+ const seeded = isSeeded(initialFeatures);
44
+ const initGenerationRef = useRef(0);
45
+ const clientRef = useRef(null);
46
+ const apiKeyRef = useRef(apiKey);
47
+ if (clientProp) {
48
+ clientRef.current = null;
49
+ } else if (apiKey) {
50
+ if (!clientRef.current || apiKeyRef.current !== apiKey) {
51
+ clientRef.current?.close();
52
+ clientRef.current = new FeatureToggle({
53
+ apiKey,
54
+ initialFeatures,
55
+ initialEtag
56
+ });
57
+ apiKeyRef.current = apiKey;
58
+ }
59
+ } else {
60
+ throw new Error("FeatureToggleProvider requires apiKey or client");
61
+ }
62
+ const client = clientProp ?? clientRef.current;
63
+ const [loading, setLoading] = useState(() => !seeded);
64
+ const [error, setError] = useState(null);
65
+ const runInit = useCallback(async () => {
66
+ const generation = ++initGenerationRef.current;
67
+ setError(null);
68
+ if (!seeded) {
69
+ setLoading(true);
70
+ }
71
+ try {
72
+ await client.init();
73
+ if (initGenerationRef.current !== generation) return;
74
+ setLoading(false);
75
+ } catch (unknownError) {
76
+ if (initGenerationRef.current !== generation) return;
77
+ const nextError = unknownError instanceof Error ? unknownError : new Error(String(unknownError));
78
+ setError(nextError);
79
+ setLoading(false);
80
+ throw nextError;
81
+ }
82
+ }, [client, seeded]);
83
+ const refresh = useCallback(async () => {
84
+ await client.refresh();
85
+ }, [client]);
86
+ useEffect(() => {
87
+ if (!autoInit) return;
88
+ void runInit().catch(() => void 0);
89
+ }, [autoInit, runInit]);
90
+ useEffect(() => {
91
+ if (!ownsClient) return;
92
+ return () => {
93
+ initGenerationRef.current += 1;
94
+ clientRef.current?.close();
95
+ clientRef.current = null;
96
+ };
97
+ }, [ownsClient]);
98
+ const contextValue = useMemo(
99
+ () => ({
100
+ client,
101
+ loading,
102
+ error,
103
+ init: runInit,
104
+ refresh
105
+ }),
106
+ [client, loading, error, runInit, refresh]
107
+ );
108
+ return /* @__PURE__ */ jsx(FeatureToggleContext.Provider, { value: contextValue, children });
109
+ }
110
+ function useFeature(key) {
111
+ const { client, loading, error } = useFeatureToggleContext();
112
+ const snapshot = useSyncExternalStore(
113
+ (listener) => client.subscribe(listener),
114
+ () => JSON.stringify({
115
+ enabled: client.isEnabled(key),
116
+ value: client.getValue(key)
117
+ }),
118
+ () => JSON.stringify({ enabled: false, value: void 0 })
119
+ );
120
+ const parsed = JSON.parse(snapshot);
121
+ return {
122
+ enabled: parsed.enabled,
123
+ value: parsed.value,
124
+ loading,
125
+ error
126
+ };
127
+ }
128
+ function useFeatureToggle() {
129
+ const { client, loading, error, init, refresh } = useFeatureToggleContext();
130
+ useSyncExternalStore(
131
+ (listener) => client.subscribe(listener),
132
+ () => JSON.stringify(client.getFeatures()),
133
+ () => "[]"
134
+ );
135
+ return {
136
+ init,
137
+ refresh,
138
+ getFeatures: (options) => client.getFeatures(options),
139
+ loading,
140
+ error
141
+ };
142
+ }
143
+ export {
144
+ FeatureToggleProvider,
145
+ useFeature,
146
+ useFeatureToggle
147
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "featuretoggle-sdk-react",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/feature-toggle/sdk-react.git"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "test": "bun run --filter featuretoggle-sdk-typescript build && bun test --preload ./test/preload.ts",
24
+ "lint": "eslint .",
25
+ "check-types": "tsc --noEmit"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=18",
29
+ "featuretoggle-sdk-typescript": "^1.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@eslint/js": "^9.28.0",
33
+ "@testing-library/react": "^16.3.0",
34
+ "@types/react": "^19.2.0",
35
+ "@happy-dom/global-registrator": "^20.0.10",
36
+ "eslint": "^9.28.0",
37
+ "featuretoggle-sdk-typescript": "workspace:*",
38
+ "react": "^19.2.0",
39
+ "react-dom": "^19.2.0",
40
+ "tsup": "^8.5.0",
41
+ "typescript": "^5.8.3",
42
+ "typescript-eslint": "^8.33.1"
43
+ }
44
+ }