@flagdown/react 0.0.2 → 0.0.4
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 +42 -25
- package/dist/index.d.ts +16 -21
- package/dist/index.js +70 -15
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,42 +1,59 @@
|
|
|
1
1
|
# @flagdown/react
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
flags.
|
|
3
|
+
Use the `Feature` component to switch between new UI and a fallback in React 18+.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
```bash
|
|
9
|
-
npm install @flagdown/react
|
|
5
|
+
```sh
|
|
6
|
+
npm install @flagdown/react@0.0.4
|
|
10
7
|
```
|
|
11
8
|
|
|
12
|
-
## Current bootstrap API
|
|
13
|
-
|
|
14
|
-
Wrap the part of the React tree that reads flags in `FlagdownProvider`, then
|
|
15
|
-
read a flag with `useFlag`:
|
|
16
|
-
|
|
17
9
|
```tsx
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
function BetaFeature() {
|
|
21
|
-
return useFlag("beta-feature") ? <p>Enabled</p> : null;
|
|
22
|
-
}
|
|
10
|
+
import { Feature } from '@flagdown/react';
|
|
23
11
|
|
|
24
|
-
export function
|
|
12
|
+
export function Checkout() {
|
|
25
13
|
return (
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
<Feature
|
|
15
|
+
flag="new-checkout"
|
|
16
|
+
fallback={<OldCheckout />}
|
|
17
|
+
>
|
|
18
|
+
<NewCheckout />
|
|
19
|
+
</Feature>
|
|
29
20
|
);
|
|
30
21
|
}
|
|
31
22
|
```
|
|
32
23
|
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
`Feature` renders its children when the named flag is on. When it is off, unknown,
|
|
25
|
+
still loading, or unavailable, it renders `fallback`. Omit `fallback` to render
|
|
26
|
+
nothing in those cases. Only the selected branch mounts; `Feature` adds no DOM
|
|
27
|
+
wrapper.
|
|
35
28
|
|
|
36
|
-
|
|
29
|
+
Configure the project once at your app root:
|
|
37
30
|
|
|
38
31
|
```tsx
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
import { FlagdownProvider } from '@flagdown/react';
|
|
33
|
+
|
|
34
|
+
<FlagdownProvider
|
|
35
|
+
apiUrl="https://api.flagdown.dev"
|
|
36
|
+
clientToken="COPY_PUBLIC_CLIENT_TOKEN_FROM_DASHBOARD"
|
|
37
|
+
>
|
|
38
|
+
<App />
|
|
41
39
|
</FlagdownProvider>
|
|
42
40
|
```
|
|
41
|
+
|
|
42
|
+
Create a project and flag in the dashboard, then copy its integration snippet.
|
|
43
|
+
`apiUrl` is the API base URL without `/v1`. `Feature` must be inside the provider.
|
|
44
|
+
|
|
45
|
+
Flags refresh every 10 seconds, without reloading the page. Set
|
|
46
|
+
`refreshIntervalMs` to change this (minimum 1000ms). Requests time out after 10
|
|
47
|
+
seconds. Initial values, unknown flags and refresh failures evaluate off;
|
|
48
|
+
a successful later refresh restores values. An optional `onError(error)` callback
|
|
49
|
+
can report failures. Changing project configuration immediately discards the
|
|
50
|
+
previous project's values. Unmounting cancels in-flight work and future refreshes.
|
|
51
|
+
|
|
52
|
+
Client tokens are public, project-scoped read configuration. They cannot manage
|
|
53
|
+
flags. Replacing a token in the dashboard invalidates the previous token; update
|
|
54
|
+
apps using it. Client-side flags are not an authorization boundary.
|
|
55
|
+
|
|
56
|
+
`Feature` is the SDK's flag consumption API. The evaluation context and evaluator
|
|
57
|
+
are internal; the package exposes no flag hooks or direct evaluation functions.
|
|
58
|
+
Use `FlagdownProvider` to configure your project. A provider without project
|
|
59
|
+
configuration renders each feature's fallback.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { PropsWithChildren } from 'react';
|
|
2
|
+
import { PropsWithChildren, ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* It deliberately keeps every flag disabled so applications can install the
|
|
12
|
-
* provider and hook safely before remote evaluation is available.
|
|
13
|
-
*/
|
|
14
|
-
declare const disabledFlagEvaluator: FlagEvaluator;
|
|
4
|
+
type FeatureProps = PropsWithChildren<{
|
|
5
|
+
/** Key of the flag controlling this feature. */
|
|
6
|
+
flag: string;
|
|
7
|
+
/** Rendered while the flag is off, unknown, loading, or unavailable. */
|
|
8
|
+
fallback?: ReactNode;
|
|
9
|
+
}>;
|
|
10
|
+
declare function Feature({ flag, fallback, children }: FeatureProps): react.JSX.Element;
|
|
15
11
|
|
|
16
12
|
interface FlagdownProviderProps extends PropsWithChildren {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
apiUrl?: string;
|
|
14
|
+
/** Public, read-only project token copied from the dashboard. */
|
|
15
|
+
clientToken?: string;
|
|
16
|
+
/** Refresh interval; minimum 1 second, default 10 seconds. */
|
|
17
|
+
refreshIntervalMs?: number;
|
|
18
|
+
onError?: (error: Error) => void;
|
|
22
19
|
}
|
|
23
|
-
declare function FlagdownProvider({ children,
|
|
24
|
-
|
|
25
|
-
declare function useFlag(flagKey: string): boolean;
|
|
20
|
+
declare function FlagdownProvider({ children, apiUrl, clientToken, refreshIntervalMs, onError }: FlagdownProviderProps): react.JSX.Element;
|
|
26
21
|
|
|
27
|
-
export { type
|
|
22
|
+
export { Feature, type FeatureProps, FlagdownProvider, type FlagdownProviderProps };
|
package/dist/index.js
CHANGED
|
@@ -1,29 +1,84 @@
|
|
|
1
|
+
// src/Feature.tsx
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
|
|
1
4
|
// src/FlagdownContext.tsx
|
|
2
|
-
import { createContext
|
|
5
|
+
import { createContext } from "react";
|
|
3
6
|
var FlagdownContext = createContext(void 0);
|
|
4
|
-
|
|
7
|
+
|
|
8
|
+
// src/Feature.tsx
|
|
9
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
10
|
+
function Feature({ flag, fallback = null, children }) {
|
|
5
11
|
const evaluator = useContext(FlagdownContext);
|
|
6
|
-
if (!evaluator)
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
return evaluator.isEnabled(flagKey);
|
|
12
|
+
if (!evaluator) throw new Error("Feature must be used within a FlagdownProvider");
|
|
13
|
+
return /* @__PURE__ */ jsx(Fragment, { children: evaluator.isEnabled(flag) ? children : fallback });
|
|
10
14
|
}
|
|
11
15
|
|
|
16
|
+
// src/FlagdownProvider.tsx
|
|
17
|
+
import { useEffect, useMemo, useState, useRef } from "react";
|
|
18
|
+
|
|
12
19
|
// src/flags.ts
|
|
13
20
|
var disabledFlagEvaluator = {
|
|
14
21
|
isEnabled: () => false
|
|
15
22
|
};
|
|
16
23
|
|
|
24
|
+
// src/remote.ts
|
|
25
|
+
async function fetchFlagState(apiUrl, clientToken, signal) {
|
|
26
|
+
const response = await fetch(apiUrl.replace(/\/$/, "") + "/v1/client/flags", {
|
|
27
|
+
headers: { "x-flagdown-client-token": clientToken },
|
|
28
|
+
cache: "no-store",
|
|
29
|
+
signal
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) throw new Error(`Flagdown request failed (${response.status})`);
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
if (!data || typeof data !== "object" || !("flags" in data) || !data.flags || typeof data.flags !== "object" || Array.isArray(data.flags) || Object.values(data.flags).some((value) => typeof value !== "boolean")) {
|
|
34
|
+
throw new Error("Invalid Flagdown response");
|
|
35
|
+
}
|
|
36
|
+
return data.flags;
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
// src/FlagdownProvider.tsx
|
|
18
|
-
import { jsx } from "react/jsx-runtime";
|
|
19
|
-
function FlagdownProvider({
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
41
|
+
function FlagdownProvider({ children, apiUrl, clientToken, refreshIntervalMs = 1e4, onError }) {
|
|
42
|
+
const errorHandler = useRef(onError);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
errorHandler.current = onError;
|
|
45
|
+
}, [onError]);
|
|
46
|
+
const [snapshot, setSnapshot] = useState();
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!apiUrl || !clientToken) return;
|
|
49
|
+
let active = true;
|
|
50
|
+
let timer;
|
|
51
|
+
let controller;
|
|
52
|
+
async function refresh() {
|
|
53
|
+
controller = new AbortController();
|
|
54
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
55
|
+
try {
|
|
56
|
+
const flags = await fetchFlagState(apiUrl, clientToken, controller.signal);
|
|
57
|
+
if (active) setSnapshot({ apiUrl, clientToken, flags });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (active) {
|
|
60
|
+
setSnapshot(void 0);
|
|
61
|
+
try {
|
|
62
|
+
errorHandler.current?.(error instanceof Error ? error : new Error("Flagdown request failed"));
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
clearTimeout(timeout);
|
|
68
|
+
if (active) timer = setTimeout(() => void refresh(), Number.isFinite(refreshIntervalMs) ? Math.max(1e3, refreshIntervalMs) : 1e4);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
void refresh();
|
|
72
|
+
return () => {
|
|
73
|
+
active = false;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
controller?.abort();
|
|
76
|
+
};
|
|
77
|
+
}, [apiUrl, clientToken, refreshIntervalMs]);
|
|
78
|
+
const value = useMemo(() => snapshot && snapshot.apiUrl === apiUrl && snapshot.clientToken === clientToken ? { isEnabled: (key) => Object.hasOwn(snapshot.flags, key) && snapshot.flags[key] === true } : disabledFlagEvaluator, [snapshot, apiUrl, clientToken]);
|
|
79
|
+
return /* @__PURE__ */ jsx2(FlagdownContext.Provider, { value, children });
|
|
24
80
|
}
|
|
25
81
|
export {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
useFlag
|
|
82
|
+
Feature,
|
|
83
|
+
FlagdownProvider
|
|
29
84
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flagdown/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "React SDK for evaluating Flagdown feature flags.",
|
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
"types": "./dist/index.d.ts"
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
|
-
"files": [
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
26
30
|
"scripts": {
|
|
27
31
|
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
28
32
|
"test": "bun test",
|