@vexillo/react-sdk 1.0.7 → 2.0.2

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,175 @@
1
+ # @vexillo/react-sdk
2
+
3
+ React bindings for [Vexillo](https://vexillo-web.vercel.app) — a self-hosted feature flag service.
4
+
5
+ ## Requirements
6
+
7
+ - React 19+
8
+ - A running Vexillo deployment
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ npm install @vexillo/react-sdk
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ### SPA (no SSR)
19
+
20
+ Wrap your app with `<VexilloProvider>` inside a `<Suspense>` boundary. The provider fetches flags on mount and suspends the subtree until they resolve.
21
+
22
+ ```tsx
23
+ import { Suspense } from "react";
24
+ import { VexilloProvider, useFlag } from "@vexillo/react-sdk";
25
+
26
+ function App() {
27
+ return (
28
+ <Suspense fallback={<Spinner />}>
29
+ <VexilloProvider
30
+ baseUrl="https://your-vexillo.example.com"
31
+ apiKey="your-api-key"
32
+ fallbacks={{ "new-checkout": false }}
33
+ >
34
+ <MyApp />
35
+ </VexilloProvider>
36
+ </Suspense>
37
+ );
38
+ }
39
+
40
+ function MyApp() {
41
+ const newCheckout = useFlag("new-checkout");
42
+ return newCheckout ? <NewCheckout /> : <OldCheckout />;
43
+ }
44
+ ```
45
+
46
+ ---
47
+
48
+ ### Next.js App Router (RSC)
49
+
50
+ Fetch flags in your server component and pass them as `initialFlags`. The provider renders synchronously on the server with no Suspense needed.
51
+
52
+ ```tsx
53
+ // app/layout.tsx
54
+ import { fetchFlags, VexilloProvider } from "@vexillo/react-sdk";
55
+
56
+ export default async function RootLayout({ children }) {
57
+ const flags = await fetchFlags(
58
+ process.env.VEXILLO_BASE_URL,
59
+ process.env.VEXILLO_API_KEY,
60
+ );
61
+
62
+ return (
63
+ <html>
64
+ <body>
65
+ <VexilloProvider
66
+ baseUrl={process.env.VEXILLO_BASE_URL}
67
+ apiKey={process.env.VEXILLO_API_KEY}
68
+ initialFlags={flags}
69
+ >
70
+ {children}
71
+ </VexilloProvider>
72
+ </body>
73
+ </html>
74
+ );
75
+ }
76
+ ```
77
+
78
+ Client components use `useFlag` as normal:
79
+
80
+ ```tsx
81
+ "use client";
82
+ import { useFlag } from "@vexillo/react-sdk";
83
+
84
+ export function CheckoutButton() {
85
+ const newCheckout = useFlag("new-checkout");
86
+ return newCheckout ? <NewCheckoutButton /> : <OldCheckoutButton />;
87
+ }
88
+ ```
89
+
90
+ ---
91
+
92
+ ### Node.js SSR with `renderToPipeableStream`
93
+
94
+ Suspense is supported — `initialFlags` is optional. The provider suspends inline and the resolved flags are streamed to the client.
95
+
96
+ ```tsx
97
+ import { renderToPipeableStream } from "react-dom/server";
98
+ import { VexilloProvider } from "@vexillo/react-sdk";
99
+
100
+ const { pipe } = renderToPipeableStream(
101
+ <Suspense fallback={<Spinner />}>
102
+ <VexilloProvider baseUrl={BASE_URL} apiKey={API_KEY}>
103
+ <App />
104
+ </VexilloProvider>
105
+ </Suspense>,
106
+ );
107
+
108
+ pipe(res);
109
+ ```
110
+
111
+ Or pass `initialFlags` to skip the suspension entirely:
112
+
113
+ ```tsx
114
+ import { fetchFlags, VexilloProvider } from "@vexillo/react-sdk";
115
+
116
+ const flags = await fetchFlags(BASE_URL, API_KEY);
117
+
118
+ const { pipe } = renderToPipeableStream(
119
+ <VexilloProvider baseUrl={BASE_URL} apiKey={API_KEY} initialFlags={flags}>
120
+ <App />
121
+ </VexilloProvider>,
122
+ );
123
+ ```
124
+
125
+ ---
126
+
127
+ ### Node.js SSR with `renderToString`
128
+
129
+ > **`renderToString` does not support Suspense.** You must call `fetchFlags` before rendering and pass the result as `initialFlags`, otherwise the provider will throw.
130
+
131
+ ```tsx
132
+ import { renderToString } from "react-dom/server";
133
+ import { fetchFlags, VexilloProvider } from "@vexillo/react-sdk";
134
+
135
+ // In your request handler:
136
+ const flags = await fetchFlags(BASE_URL, API_KEY);
137
+
138
+ const html = renderToString(
139
+ <VexilloProvider baseUrl={BASE_URL} apiKey={API_KEY} initialFlags={flags}>
140
+ <App />
141
+ </VexilloProvider>,
142
+ );
143
+ ```
144
+
145
+ ---
146
+
147
+ ## API
148
+
149
+ ### `<VexilloProvider>`
150
+
151
+ | Prop | Type | Required | Description |
152
+ |---|---|---|---|
153
+ | `baseUrl` | `string` | Yes | Base URL of your Vexillo deployment |
154
+ | `apiKey` | `string` | Yes | SDK API key for the target environment |
155
+ | `initialFlags` | `Record<string, boolean>` | No* | Pre-resolved flags from the server. **Required when using `renderToString`** |
156
+ | `fallbacks` | `Record<string, boolean>` | No | Default values for unknown flag keys (default: `{}`) |
157
+ | `children` | `ReactNode` | Yes | |
158
+
159
+ ### `useFlag(key: string): boolean`
160
+
161
+ Returns the current value of a feature flag. Falls back to `fallbacks[key] ?? false` for unknown keys. Must be called inside a `<VexilloProvider>`.
162
+
163
+ ### `fetchFlags(baseUrl: string, apiKey: string): Promise<Record<string, boolean>>`
164
+
165
+ Fetches flags from the Vexillo API. Use this on the server to get `initialFlags`. Returns an empty object on error — never throws.
166
+
167
+ ## Error handling
168
+
169
+ Fetch failures (network errors, non-2xx responses) silently resolve with an empty flag map. All `useFlag` calls fall back to `fallbacks[key] ?? false`. Feature flags will never crash your app.
170
+
171
+ ## Caching
172
+
173
+ On the **client**, flags are cached in memory by `baseUrl + apiKey`. The cache is invalidated when either prop changes, triggering a new fetch and re-suspension.
174
+
175
+ On the **server**, every render fetches fresh — there is no server-side cache to prevent flag data from leaking across requests in Node.js.
package/dist/index.cjs CHANGED
@@ -21,51 +21,71 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
23
  VexilloProvider: () => VexilloProvider,
24
+ fetchFlags: () => fetchFlags,
24
25
  useFlag: () => useFlag
25
26
  });
26
27
  module.exports = __toCommonJS(src_exports);
27
28
 
28
29
  // src/provider.tsx
29
30
  var import_react = require("react");
31
+
32
+ // src/fetch-flags.ts
33
+ async function fetchFlags(baseUrl, apiKey) {
34
+ try {
35
+ const res = await fetch(`${baseUrl}/api/flags`, {
36
+ headers: { Authorization: `Bearer ${apiKey}` }
37
+ });
38
+ if (!res.ok) {
39
+ console.warn(
40
+ `Vexillo: fetchFlags received status ${res.status} ${res.statusText}. Returning empty flags.`
41
+ );
42
+ return {};
43
+ }
44
+ const data = await res.json();
45
+ const map = {};
46
+ for (const f of data.flags) {
47
+ map[f.key] = f.enabled;
48
+ }
49
+ return map;
50
+ } catch (e) {
51
+ console.warn("Vexillo: fetchFlags failed. Returning empty flags.");
52
+ return {};
53
+ }
54
+ }
55
+
56
+ // src/provider.tsx
30
57
  var import_jsx_runtime = require("react/jsx-runtime");
31
58
  var VexilloContext = (0, import_react.createContext)(null);
59
+ var clientCache = /* @__PURE__ */ new Map();
60
+ function getOrCreateFlagPromise(baseUrl, apiKey) {
61
+ const key = `${baseUrl}__${apiKey}`;
62
+ if (!clientCache.has(key)) {
63
+ clientCache.set(key, fetchFlags(baseUrl, apiKey));
64
+ }
65
+ return clientCache.get(key);
66
+ }
32
67
  function VexilloProvider({
33
68
  baseUrl,
34
69
  apiKey,
70
+ initialFlags,
35
71
  fallbacks = {},
36
72
  children
37
73
  }) {
38
- const [flags, setFlags] = (0, import_react.useState)(null);
39
- const [error, setError] = (0, import_react.useState)(null);
40
- (0, import_react.useEffect)(() => {
41
- let cancelled = false;
42
- fetch(`${baseUrl}/api/flags`, {
43
- headers: { Authorization: `Bearer ${apiKey}` }
44
- }).then((res) => {
45
- if (!res.ok) {
46
- throw new Error(
47
- `Vexillo: API responded with status ${res.status} ${res.statusText}`
48
- );
49
- }
50
- return res.json();
51
- }).then((data) => {
52
- if (cancelled) return;
53
- const map = {};
54
- for (const f of data.flags) {
55
- map[f.key] = f.enabled;
56
- }
57
- setFlags(map);
58
- }).catch((err) => {
59
- if (cancelled) return;
60
- setError(
61
- err instanceof Error ? err : new Error(`Vexillo: unexpected error \u2014 ${String(err)}`)
62
- );
63
- });
64
- return () => {
65
- cancelled = true;
66
- };
67
- }, [apiKey, baseUrl]);
68
- if (error) throw error;
74
+ const isServer = typeof window === "undefined";
75
+ if (initialFlags) {
76
+ const key = `${baseUrl}__${apiKey}`;
77
+ if (!isServer && !clientCache.has(key)) {
78
+ clientCache.set(key, Promise.resolve(initialFlags));
79
+ }
80
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VexilloContext.Provider, { value: { flags: initialFlags, fallbacks }, children });
81
+ }
82
+ let flagPromise;
83
+ if (isServer) {
84
+ flagPromise = fetchFlags(baseUrl, apiKey);
85
+ } else {
86
+ flagPromise = getOrCreateFlagPromise(baseUrl, apiKey);
87
+ }
88
+ const flags = (0, import_react.use)(flagPromise);
69
89
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VexilloContext.Provider, { value: { flags, fallbacks }, children });
70
90
  }
71
91
  function useVexilloContext() {
@@ -80,7 +100,7 @@ function useVexilloContext() {
80
100
  function useFlag(key) {
81
101
  var _a;
82
102
  const { flags, fallbacks } = useVexilloContext();
83
- if (flags !== null && key in flags) {
103
+ if (key in flags) {
84
104
  return flags[key];
85
105
  }
86
106
  return (_a = fallbacks[key]) != null ? _a : false;
@@ -88,5 +108,6 @@ function useFlag(key) {
88
108
  // Annotate the CommonJS export names for ESM import in node:
89
109
  0 && (module.exports = {
90
110
  VexilloProvider,
111
+ fetchFlags,
91
112
  useFlag
92
113
  });
package/dist/index.d.cts CHANGED
@@ -5,11 +5,48 @@ interface VexilloProviderProps {
5
5
  baseUrl: string;
6
6
  /** SDK API key for the target environment */
7
7
  apiKey: string;
8
- /** Flag values used before the fetch resolves and for unknown keys */
8
+ /**
9
+ * Pre-resolved flags from the server.
10
+ *
11
+ * **Required when using `renderToString`** — pass the result of
12
+ * `fetchFlags(baseUrl, apiKey)` here, as `renderToString` does not support
13
+ * Suspense. Without this, the provider will throw during server rendering.
14
+ *
15
+ * Optional for `renderToPipeableStream`, RSC, and SPA — the provider will
16
+ * suspend and fetch flags on its own if omitted.
17
+ */
18
+ initialFlags?: Record<string, boolean>;
19
+ /** Flag values used as defaults for unknown keys */
9
20
  fallbacks?: Record<string, boolean>;
10
21
  children: ReactNode;
11
22
  }
12
- declare function VexilloProvider({ baseUrl, apiKey, fallbacks, children, }: VexilloProviderProps): React.ReactElement;
23
+ /**
24
+ * Provides feature flag values to the React tree via Suspense.
25
+ *
26
+ * The subtree suspends until flags are resolved — wrap with `<Suspense>`
27
+ * to show a loading state.
28
+ *
29
+ * ```tsx
30
+ * <Suspense fallback={<Spinner />}>
31
+ * <VexilloProvider baseUrl="..." apiKey="...">
32
+ * <App />
33
+ * </VexilloProvider>
34
+ * </Suspense>
35
+ * ```
36
+ *
37
+ * ### SSR with `renderToString`
38
+ * `renderToString` does not support Suspense. Call `fetchFlags` before
39
+ * rendering and pass the result as `initialFlags`:
40
+ * ```ts
41
+ * const flags = await fetchFlags(baseUrl, apiKey);
42
+ * renderToString(<VexilloProvider initialFlags={flags} ...>);
43
+ * ```
44
+ *
45
+ * ### SSR with `renderToPipeableStream` / RSC
46
+ * `initialFlags` is optional. The provider suspends and streams the resolved
47
+ * flags inline. Pass `initialFlags` to avoid the suspension entirely.
48
+ */
49
+ declare function VexilloProvider({ baseUrl, apiKey, initialFlags, fallbacks, children, }: VexilloProviderProps): React.ReactElement;
13
50
 
14
51
  /**
15
52
  * Returns the current boolean value for a feature flag key.
@@ -22,4 +59,32 @@ declare function VexilloProvider({ baseUrl, apiKey, fallbacks, children, }: Vexi
22
59
  */
23
60
  declare function useFlag(key: string): boolean;
24
61
 
25
- export { VexilloProvider, type VexilloProviderProps, useFlag };
62
+ /**
63
+ * Fetches feature flags from your Vexillo deployment.
64
+ *
65
+ * Use this on the server before rendering to pass resolved flags as
66
+ * `initialFlags` to `<VexilloProvider>`.
67
+ *
68
+ * ## When to use
69
+ *
70
+ * - **`renderToString`** — Suspense is not supported. You MUST call
71
+ * `fetchFlags` before rendering and pass the result as `initialFlags`:
72
+ * ```ts
73
+ * const flags = await fetchFlags(baseUrl, apiKey);
74
+ * renderToString(<VexilloProvider initialFlags={flags} ...>);
75
+ * ```
76
+ *
77
+ * - **`renderToPipeableStream` / `renderToReadableStream`** — Suspense is
78
+ * supported. `initialFlags` is optional; the provider will suspend and
79
+ * fetch flags inline. You may still pass `initialFlags` to avoid the
80
+ * suspension entirely.
81
+ *
82
+ * - **React Server Components (Next.js App Router)** — call `fetchFlags`
83
+ * in your server component and pass the result as `initialFlags`.
84
+ *
85
+ * - **SPA (no SSR)** — omit `initialFlags`. The provider fetches on the
86
+ * client and suspends until resolved.
87
+ */
88
+ declare function fetchFlags(baseUrl: string, apiKey: string): Promise<Record<string, boolean>>;
89
+
90
+ export { VexilloProvider, type VexilloProviderProps, fetchFlags, useFlag };
package/dist/index.d.ts CHANGED
@@ -5,11 +5,48 @@ interface VexilloProviderProps {
5
5
  baseUrl: string;
6
6
  /** SDK API key for the target environment */
7
7
  apiKey: string;
8
- /** Flag values used before the fetch resolves and for unknown keys */
8
+ /**
9
+ * Pre-resolved flags from the server.
10
+ *
11
+ * **Required when using `renderToString`** — pass the result of
12
+ * `fetchFlags(baseUrl, apiKey)` here, as `renderToString` does not support
13
+ * Suspense. Without this, the provider will throw during server rendering.
14
+ *
15
+ * Optional for `renderToPipeableStream`, RSC, and SPA — the provider will
16
+ * suspend and fetch flags on its own if omitted.
17
+ */
18
+ initialFlags?: Record<string, boolean>;
19
+ /** Flag values used as defaults for unknown keys */
9
20
  fallbacks?: Record<string, boolean>;
10
21
  children: ReactNode;
11
22
  }
12
- declare function VexilloProvider({ baseUrl, apiKey, fallbacks, children, }: VexilloProviderProps): React.ReactElement;
23
+ /**
24
+ * Provides feature flag values to the React tree via Suspense.
25
+ *
26
+ * The subtree suspends until flags are resolved — wrap with `<Suspense>`
27
+ * to show a loading state.
28
+ *
29
+ * ```tsx
30
+ * <Suspense fallback={<Spinner />}>
31
+ * <VexilloProvider baseUrl="..." apiKey="...">
32
+ * <App />
33
+ * </VexilloProvider>
34
+ * </Suspense>
35
+ * ```
36
+ *
37
+ * ### SSR with `renderToString`
38
+ * `renderToString` does not support Suspense. Call `fetchFlags` before
39
+ * rendering and pass the result as `initialFlags`:
40
+ * ```ts
41
+ * const flags = await fetchFlags(baseUrl, apiKey);
42
+ * renderToString(<VexilloProvider initialFlags={flags} ...>);
43
+ * ```
44
+ *
45
+ * ### SSR with `renderToPipeableStream` / RSC
46
+ * `initialFlags` is optional. The provider suspends and streams the resolved
47
+ * flags inline. Pass `initialFlags` to avoid the suspension entirely.
48
+ */
49
+ declare function VexilloProvider({ baseUrl, apiKey, initialFlags, fallbacks, children, }: VexilloProviderProps): React.ReactElement;
13
50
 
14
51
  /**
15
52
  * Returns the current boolean value for a feature flag key.
@@ -22,4 +59,32 @@ declare function VexilloProvider({ baseUrl, apiKey, fallbacks, children, }: Vexi
22
59
  */
23
60
  declare function useFlag(key: string): boolean;
24
61
 
25
- export { VexilloProvider, type VexilloProviderProps, useFlag };
62
+ /**
63
+ * Fetches feature flags from your Vexillo deployment.
64
+ *
65
+ * Use this on the server before rendering to pass resolved flags as
66
+ * `initialFlags` to `<VexilloProvider>`.
67
+ *
68
+ * ## When to use
69
+ *
70
+ * - **`renderToString`** — Suspense is not supported. You MUST call
71
+ * `fetchFlags` before rendering and pass the result as `initialFlags`:
72
+ * ```ts
73
+ * const flags = await fetchFlags(baseUrl, apiKey);
74
+ * renderToString(<VexilloProvider initialFlags={flags} ...>);
75
+ * ```
76
+ *
77
+ * - **`renderToPipeableStream` / `renderToReadableStream`** — Suspense is
78
+ * supported. `initialFlags` is optional; the provider will suspend and
79
+ * fetch flags inline. You may still pass `initialFlags` to avoid the
80
+ * suspension entirely.
81
+ *
82
+ * - **React Server Components (Next.js App Router)** — call `fetchFlags`
83
+ * in your server component and pass the result as `initialFlags`.
84
+ *
85
+ * - **SPA (no SSR)** — omit `initialFlags`. The provider fetches on the
86
+ * client and suspends until resolved.
87
+ */
88
+ declare function fetchFlags(baseUrl: string, apiKey: string): Promise<Record<string, boolean>>;
89
+
90
+ export { VexilloProvider, type VexilloProviderProps, fetchFlags, useFlag };
package/dist/index.mjs CHANGED
@@ -1,49 +1,67 @@
1
1
  // src/provider.tsx
2
2
  import {
3
3
  createContext,
4
- useContext,
5
- useEffect,
6
- useState
4
+ use,
5
+ useContext
7
6
  } from "react";
7
+
8
+ // src/fetch-flags.ts
9
+ async function fetchFlags(baseUrl, apiKey) {
10
+ try {
11
+ const res = await fetch(`${baseUrl}/api/flags`, {
12
+ headers: { Authorization: `Bearer ${apiKey}` }
13
+ });
14
+ if (!res.ok) {
15
+ console.warn(
16
+ `Vexillo: fetchFlags received status ${res.status} ${res.statusText}. Returning empty flags.`
17
+ );
18
+ return {};
19
+ }
20
+ const data = await res.json();
21
+ const map = {};
22
+ for (const f of data.flags) {
23
+ map[f.key] = f.enabled;
24
+ }
25
+ return map;
26
+ } catch (e) {
27
+ console.warn("Vexillo: fetchFlags failed. Returning empty flags.");
28
+ return {};
29
+ }
30
+ }
31
+
32
+ // src/provider.tsx
8
33
  import { jsx } from "react/jsx-runtime";
9
34
  var VexilloContext = createContext(null);
35
+ var clientCache = /* @__PURE__ */ new Map();
36
+ function getOrCreateFlagPromise(baseUrl, apiKey) {
37
+ const key = `${baseUrl}__${apiKey}`;
38
+ if (!clientCache.has(key)) {
39
+ clientCache.set(key, fetchFlags(baseUrl, apiKey));
40
+ }
41
+ return clientCache.get(key);
42
+ }
10
43
  function VexilloProvider({
11
44
  baseUrl,
12
45
  apiKey,
46
+ initialFlags,
13
47
  fallbacks = {},
14
48
  children
15
49
  }) {
16
- const [flags, setFlags] = useState(null);
17
- const [error, setError] = useState(null);
18
- useEffect(() => {
19
- let cancelled = false;
20
- fetch(`${baseUrl}/api/flags`, {
21
- headers: { Authorization: `Bearer ${apiKey}` }
22
- }).then((res) => {
23
- if (!res.ok) {
24
- throw new Error(
25
- `Vexillo: API responded with status ${res.status} ${res.statusText}`
26
- );
27
- }
28
- return res.json();
29
- }).then((data) => {
30
- if (cancelled) return;
31
- const map = {};
32
- for (const f of data.flags) {
33
- map[f.key] = f.enabled;
34
- }
35
- setFlags(map);
36
- }).catch((err) => {
37
- if (cancelled) return;
38
- setError(
39
- err instanceof Error ? err : new Error(`Vexillo: unexpected error \u2014 ${String(err)}`)
40
- );
41
- });
42
- return () => {
43
- cancelled = true;
44
- };
45
- }, [apiKey, baseUrl]);
46
- if (error) throw error;
50
+ const isServer = typeof window === "undefined";
51
+ if (initialFlags) {
52
+ const key = `${baseUrl}__${apiKey}`;
53
+ if (!isServer && !clientCache.has(key)) {
54
+ clientCache.set(key, Promise.resolve(initialFlags));
55
+ }
56
+ return /* @__PURE__ */ jsx(VexilloContext.Provider, { value: { flags: initialFlags, fallbacks }, children });
57
+ }
58
+ let flagPromise;
59
+ if (isServer) {
60
+ flagPromise = fetchFlags(baseUrl, apiKey);
61
+ } else {
62
+ flagPromise = getOrCreateFlagPromise(baseUrl, apiKey);
63
+ }
64
+ const flags = use(flagPromise);
47
65
  return /* @__PURE__ */ jsx(VexilloContext.Provider, { value: { flags, fallbacks }, children });
48
66
  }
49
67
  function useVexilloContext() {
@@ -58,12 +76,13 @@ function useVexilloContext() {
58
76
  function useFlag(key) {
59
77
  var _a;
60
78
  const { flags, fallbacks } = useVexilloContext();
61
- if (flags !== null && key in flags) {
79
+ if (key in flags) {
62
80
  return flags[key];
63
81
  }
64
82
  return (_a = fallbacks[key]) != null ? _a : false;
65
83
  }
66
84
  export {
67
85
  VexilloProvider,
86
+ fetchFlags,
68
87
  useFlag
69
88
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vexillo/react-sdk",
3
- "version": "1.0.7",
3
+ "version": "2.0.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -16,11 +16,12 @@
16
16
  }
17
17
  },
18
18
  "files": [
19
- "dist"
19
+ "dist",
20
+ "README.md"
20
21
  ],
21
22
  "peerDependencies": {
22
- "react": "^18 || ^19",
23
- "react-dom": "^18 || ^19"
23
+ "react": "^19",
24
+ "react-dom": "^19"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@testing-library/react": "^16",