flagmint-monorepo 1.0.11

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.
@@ -0,0 +1,31 @@
1
+ name: Publish Package
2
+
3
+ on:
4
+ release:
5
+ types: [published] # add "prereleased" if you publish pre-releases too
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - uses: pnpm/action-setup@v3
15
+ with:
16
+ version: 9
17
+ run_install: false
18
+
19
+ - uses: actions/setup-node@v4
20
+ with:
21
+ node-version: 20
22
+ registry-url: https://registry.npmjs.org
23
+ cache: pnpm
24
+
25
+ - run: pnpm -v # sanity check
26
+ - run: pnpm install --frozen-lockfile
27
+ - run: pnpm build
28
+ - run: pnpm test || true
29
+ - run: npm publish --access public
30
+ env:
31
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Israel Edet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,609 @@
1
+ # flagmint-react-sdk
2
+
3
+ A React wrapper for [flagmint-js-sdk](https://www.npmjs.com/package/flagmint-js-sdk) that provides React-specific hooks and context with fine-grained reactivity powered by Zustand.
4
+
5
+ ## ✨ Features
6
+
7
+ - 🎯 **Fine-grained reactivity** - Only components using specific flags re-render when those flags change
8
+ - 🚀 **SSR-safe** - No global state leakage between server requests
9
+ - 📦 **TypeScript support** - Full type safety with generics for flag values and context
10
+ - 🔀 **Dual export strategy** - Separate server-safe types and client-only hooks
11
+ - ⚡ **Auto-initialization** - Direct integration with FlagClient from flagmint-js-sdk
12
+ - 🔐 **Deferred initialization** - Perfect for authentication flows
13
+ - 📱 **Framework agnostic** - Works with Next.js, Remix, Vite, and more
14
+
15
+ ## 📦 Installation
16
+
17
+ ```bash
18
+ npm install react-flagmint flagmint-js-sdk
19
+ # or
20
+ pnpm add react-flagmint flagmint-js-sdk
21
+ # or
22
+ yarn add react-flagmint flagmint-js-sdk
23
+ ```
24
+
25
+ ## 🚀 Quick Start
26
+
27
+ ### 1. Basic Setup
28
+
29
+ ```tsx
30
+ // app/providers.tsx (Next.js App Router)
31
+ 'use client'
32
+ import { FlagmintProvider } from 'react-flagmint/client'
33
+
34
+ export default function Providers({ children }: { children: React.ReactNode }) {
35
+ return (
36
+ <FlagmintProvider
37
+ options={{
38
+ apiKey: process.env.NEXT_PUBLIC_FLAGMINT_API_KEY!,
39
+ transportMode: 'websocket',
40
+ context: {
41
+ userId: 'user-123',
42
+ plan: 'premium'
43
+ }
44
+ }}
45
+ >
46
+ {children}
47
+ </FlagmintProvider>
48
+ )
49
+ }
50
+ ```
51
+
52
+ ```tsx
53
+ // app/layout.tsx
54
+ import Providers from './providers'
55
+
56
+ export default function RootLayout({
57
+ children,
58
+ }: {
59
+ children: React.ReactNode
60
+ }) {
61
+ return (
62
+ <html>
63
+ <body>
64
+ <Providers>
65
+ {children}
66
+ </Providers>
67
+ </body>
68
+ </html>
69
+ )
70
+ }
71
+ ```
72
+
73
+ ### 2. Using Flags in Components
74
+
75
+ ```tsx
76
+ // components/FeatureComponent.tsx
77
+ 'use client'
78
+ import { useFlag, useFlags, useFlagmintReady } from 'react-flagmint/client'
79
+
80
+ export default function FeatureComponent() {
81
+ const showNewFeature = useFlag('new-dashboard', false)
82
+ const userPlan = useFlag('user-plan', 'free')
83
+ const isReady = useFlagmintReady()
84
+
85
+ if (!isReady) {
86
+ return <div>Loading flags...</div>
87
+ }
88
+
89
+ return (
90
+ <div>
91
+ {showNewFeature && <NewDashboard />}
92
+ <div>Current plan: {userPlan}</div>
93
+ </div>
94
+ )
95
+ }
96
+ ```
97
+
98
+ ## 📚 API Reference
99
+
100
+ ### FlagmintProvider
101
+
102
+ The provider component that initializes the FlagClient and provides flag context to your app.
103
+
104
+ ```tsx
105
+ <FlagmintProvider
106
+ options={FlagClientOptions}
107
+ initialFlags={{}}
108
+ deferInitialization={false}
109
+ >
110
+ {children}
111
+ </FlagmintProvider>
112
+ ```
113
+
114
+ #### Props
115
+
116
+ | Prop | Type | Default | Description |
117
+ |------|------|---------|-------------|
118
+ | `options` | `FlagClientOptions` | Required | Configuration for the FlagClient |
119
+ | `initialFlags` | `Flags` | `{}` | Initial flag values for SSR/hydration |
120
+ | `deferInitialization` | `boolean` | `false` | If true, wait for manual initialization |
121
+
122
+ #### FlagClientOptions
123
+
124
+ ```tsx
125
+ interface FlagClientOptions<C extends Record<string, any> = Record<string, any>> {
126
+ apiKey: string
127
+ context?: C
128
+ transportMode?: 'auto' | 'websocket' | 'long-polling'
129
+ enableOfflineCache?: boolean
130
+ persistContext?: boolean
131
+ onError?: (error: Error) => void
132
+ previewMode?: boolean
133
+ rawFlags?: Record<string, FlagValue>
134
+ deferInitialization?: boolean
135
+ }
136
+ ```
137
+
138
+ ### Hooks
139
+
140
+ #### useFlag(key, fallback?)
141
+
142
+ Get a specific flag value with fine-grained reactivity.
143
+
144
+ ```tsx
145
+ const showFeature = useFlag('feature-name', false)
146
+ const userTier = useFlag('user-tier', 'free')
147
+ const config = useFlag('app-config', { theme: 'light' })
148
+ ```
149
+
150
+ **Parameters:**
151
+ - `key: string` - The flag key
152
+ - `fallback?: T` - Default value if flag is not found
153
+
154
+ **Returns:** The flag value or fallback
155
+
156
+ #### useFlags()
157
+
158
+ Get all currently loaded flags.
159
+
160
+ ```tsx
161
+ const allFlags = useFlags()
162
+ console.log(allFlags) // { 'feature-a': true, 'user-tier': 'pro' }
163
+ ```
164
+
165
+ **Returns:** Object containing all loaded flags
166
+
167
+ #### useFlagmint()
168
+
169
+ Get access to the FlagClient instance and utilities.
170
+
171
+ ```tsx
172
+ const { client, isReady, isInitialized, updateContext } = useFlagmint()
173
+
174
+ // Update user context
175
+ await updateContext({ userId: 'new-user', plan: 'enterprise' })
176
+ ```
177
+
178
+ **Returns:**
179
+ ```tsx
180
+ {
181
+ client: FlagClient | null
182
+ isReady: boolean
183
+ isInitialized: boolean
184
+ updateContext: (context: Record<string, any>) => Promise<void>
185
+ }
186
+ ```
187
+
188
+ #### useFlagmintReady()
189
+
190
+ Check if the FlagClient is ready to serve flags.
191
+
192
+ ```tsx
193
+ const isReady = useFlagmintReady()
194
+
195
+ if (!isReady) {
196
+ return <LoadingSpinner />
197
+ }
198
+ ```
199
+
200
+ #### useFlagClient()
201
+
202
+ Get direct access to the FlagClient instance.
203
+
204
+ ```tsx
205
+ const client = useFlagClient<MyFlagTypes>()
206
+ const specificFlag = client?.getFlag('feature-x', false)
207
+ ```
208
+
209
+ ## 🔧 Usage Patterns
210
+
211
+ ### 1. Auto-initialization (Default)
212
+
213
+ Best when user context is available immediately:
214
+
215
+ ```tsx
216
+ function App() {
217
+ return (
218
+ <FlagmintProvider
219
+ options={{
220
+ apiKey: 'your-api-key',
221
+ context: {
222
+ userId: getCurrentUser().id,
223
+ plan: getCurrentUser().plan
224
+ }
225
+ }}
226
+ >
227
+ <Dashboard />
228
+ </FlagmintProvider>
229
+ )
230
+ }
231
+ ```
232
+
233
+ ### 2. Deferred Initialization
234
+
235
+ Perfect for authentication flows:
236
+
237
+ ```tsx
238
+ // Provider with deferred initialization
239
+ <FlagmintProvider
240
+ options={{
241
+ apiKey: 'your-api-key',
242
+ context: {} // Empty initially
243
+ }}
244
+ deferInitialization={true}
245
+ >
246
+ <App />
247
+ </FlagmintProvider>
248
+
249
+ // Login component
250
+ function LoginPage() {
251
+ const { updateContext } = useFlagmint()
252
+
253
+ const handleLogin = async (user) => {
254
+ // First update context with user info
255
+ await updateContext({
256
+ userId: user.id,
257
+ plan: user.plan,
258
+ locale: user.locale
259
+ })
260
+
261
+ // Flags are now available with user context
262
+ navigate('/dashboard')
263
+ }
264
+
265
+ return <LoginForm onLogin={handleLogin} />
266
+ }
267
+ ```
268
+
269
+ ### 3. TypeScript Usage
270
+
271
+ Define your flag types for better type safety:
272
+
273
+ ```tsx
274
+ // types/flags.ts
275
+ export interface AppFlags {
276
+ 'show-beta-feature': boolean
277
+ 'user-plan': 'free' | 'pro' | 'enterprise'
278
+ 'theme-config': {
279
+ primaryColor: string
280
+ darkMode: boolean
281
+ }
282
+ }
283
+
284
+ export interface UserContext {
285
+ userId: string
286
+ plan: string
287
+ locale: string
288
+ }
289
+
290
+ // components/TypedComponent.tsx
291
+ import { useFlag } from 'react-flagmint/client'
292
+ import type { AppFlags } from '../types/flags'
293
+
294
+ export default function TypedComponent() {
295
+ // TypeScript knows this returns boolean
296
+ const showBeta = useFlag<AppFlags['show-beta-feature']>('show-beta-feature', false)
297
+
298
+ // TypeScript knows this returns the union type
299
+ const plan = useFlag<AppFlags['user-plan']>('user-plan', 'free')
300
+
301
+ return (
302
+ <div>
303
+ {showBeta && <BetaFeature />}
304
+ <div>Plan: {plan}</div>
305
+ </div>
306
+ )
307
+ }
308
+ ```
309
+
310
+ ### 4. Server-Side Rendering (SSR)
311
+
312
+ #### Next.js App Router
313
+
314
+ ```tsx
315
+ // app/providers.tsx
316
+ 'use client'
317
+ import { FlagmintProvider } from 'react-flagmint/client'
318
+
319
+ export default function Providers({ children, initialFlags = {} }) {
320
+ return (
321
+ <FlagmintProvider
322
+ options={{
323
+ apiKey: process.env.NEXT_PUBLIC_FLAGMINT_API_KEY!,
324
+ transportMode: 'websocket'
325
+ }}
326
+ initialFlags={initialFlags}
327
+ >
328
+ {children}
329
+ </FlagmintProvider>
330
+ )
331
+ }
332
+
333
+ // app/page.tsx
334
+ import { FlagClient } from 'flagmint-js-sdk'
335
+ import Providers from './providers'
336
+ import ClientComponent from './ClientComponent'
337
+
338
+ export default async function Page() {
339
+ // Optionally preload flags on server
340
+ let initialFlags = {}
341
+
342
+ try {
343
+ const serverClient = new FlagClient({
344
+ apiKey: process.env.FLAGMINT_API_KEY!,
345
+ context: { server: true }
346
+ })
347
+ await serverClient.ready()
348
+ initialFlags = await serverClient.getFlags() // If this method exists
349
+ } catch (error) {
350
+ console.warn('Failed to load initial flags:', error)
351
+ }
352
+
353
+ return (
354
+ <Providers initialFlags={initialFlags}>
355
+ <ClientComponent />
356
+ </Providers>
357
+ )
358
+ }
359
+ ```
360
+
361
+ ### 5. Context Updates
362
+
363
+ Update user context dynamically:
364
+
365
+ ```tsx
366
+ function UserSettings() {
367
+ const { updateContext } = useFlagmint()
368
+ const currentTheme = useFlag('user-theme', 'light')
369
+
370
+ const handleThemeChange = async (newTheme: string) => {
371
+ // Update context - flags will be re-evaluated
372
+ await updateContext({ theme: newTheme })
373
+ }
374
+
375
+ const handlePlanUpgrade = async (newPlan: string) => {
376
+ await updateContext({ plan: newPlan })
377
+ // Component will re-render with new plan-based flags
378
+ }
379
+
380
+ return (
381
+ <div>
382
+ <ThemeSelector onChange={handleThemeChange} />
383
+ <PlanUpgrade onUpgrade={handlePlanUpgrade} />
384
+ </div>
385
+ )
386
+ }
387
+ ```
388
+
389
+ ### 6. Loading States
390
+
391
+ Handle loading states gracefully:
392
+
393
+ ```tsx
394
+ function FeaturePage() {
395
+ const isReady = useFlagmintReady()
396
+ const showPremiumFeature = useFlag('premium-feature', false)
397
+
398
+ if (!isReady) {
399
+ return (
400
+ <div className="loading-container">
401
+ <Spinner />
402
+ <p>Loading personalized features...</p>
403
+ </div>
404
+ )
405
+ }
406
+
407
+ return (
408
+ <div>
409
+ <h1>Features</h1>
410
+ {showPremiumFeature ? (
411
+ <PremiumFeature />
412
+ ) : (
413
+ <UpgradeBanner />
414
+ )}
415
+ </div>
416
+ )
417
+ }
418
+ ```
419
+
420
+ ### 7. Error Handling
421
+
422
+ ```tsx
423
+ <FlagmintProvider
424
+ options={{
425
+ apiKey: 'your-api-key',
426
+ onError: (error) => {
427
+ console.error('Flagmint error:', error)
428
+ // Send to error reporting service
429
+ errorReporting.captureException(error)
430
+ }
431
+ }}
432
+ >
433
+ <App />
434
+ </FlagmintProvider>
435
+ ```
436
+
437
+ ## 🏗️ Framework Examples
438
+
439
+ ### Next.js (App Router)
440
+
441
+ ```tsx
442
+ // app/layout.tsx
443
+ import Providers from './providers'
444
+
445
+ export default function RootLayout({ children }) {
446
+ return (
447
+ <html lang="en">
448
+ <body>
449
+ <Providers>{children}</Providers>
450
+ </body>
451
+ </html>
452
+ )
453
+ }
454
+
455
+ // app/providers.tsx
456
+ 'use client'
457
+ import { FlagmintProvider } from 'react-flagmint/client'
458
+
459
+ export default function Providers({ children }) {
460
+ return (
461
+ <FlagmintProvider
462
+ options={{
463
+ apiKey: process.env.NEXT_PUBLIC_FLAGMINT_API_KEY!,
464
+ transportMode: 'long-polling'
465
+ }}
466
+ >
467
+ {children}
468
+ </FlagmintProvider>
469
+ )
470
+ }
471
+ ```
472
+
473
+ ### Vite + React
474
+
475
+ ```tsx
476
+ // src/main.tsx
477
+ import React from 'react'
478
+ import ReactDOM from 'react-dom/client'
479
+ import { FlagmintProvider } from 'react-flagmint/client'
480
+ import App from './App'
481
+
482
+ ReactDOM.createRoot(document.getElementById('root')!).render(
483
+ <React.StrictMode>
484
+ <FlagmintProvider
485
+ options={{
486
+ apiKey: import.meta.env.VITE_FLAGMINT_API_KEY,
487
+ transportMode: 'websocket'
488
+ }}
489
+ >
490
+ <App />
491
+ </FlagmintProvider>
492
+ </React.StrictMode>
493
+ )
494
+ ```
495
+
496
+ ### Remix
497
+
498
+ ```tsx
499
+ // app/root.tsx
500
+ import { FlagmintProvider } from 'react-flagmint/client'
501
+
502
+ export default function App() {
503
+ return (
504
+ <html>
505
+ <head>
506
+ <Meta />
507
+ <Links />
508
+ </head>
509
+ <body>
510
+ <FlagmintProvider
511
+ options={{
512
+ apiKey: process.env.FLAGMINT_API_KEY!,
513
+ transportMode: 'long-polling'
514
+ }}
515
+ >
516
+ <Outlet />
517
+ </FlagmintProvider>
518
+ <Scripts />
519
+ </body>
520
+ </html>
521
+ )
522
+ }
523
+ ```
524
+
525
+ ## ⚡ Performance
526
+
527
+ ### Fine-grained Reactivity
528
+
529
+ Only components that use specific flags will re-render when those flags change:
530
+
531
+ ```tsx
532
+ // ✅ Good: Only re-renders when 'feature-a' changes
533
+ function ComponentA() {
534
+ const featureA = useFlag('feature-a', false)
535
+ return <div>{featureA && <FeatureA />}</div>
536
+ }
537
+
538
+ // ✅ Good: Only re-renders when 'feature-b' changes
539
+ function ComponentB() {
540
+ const featureB = useFlag('feature-b', false)
541
+ return <div>{featureB && <FeatureB />}</div>
542
+ }
543
+
544
+ // ❌ Avoid: Re-renders when ANY flag changes
545
+ function ComponentAll() {
546
+ const allFlags = useFlags()
547
+ return <div>{allFlags['feature-a'] && <FeatureA />}</div>
548
+ }
549
+ ```
550
+
551
+ ### Bundle Size
552
+
553
+ - **react-flagmint**: ~3KB gzipped
554
+ - **zustand**: ~2KB gzipped
555
+ - **Total overhead**: ~5KB gzipped
556
+
557
+ ## 🔧 Development
558
+
559
+ ### Local Development
560
+
561
+ ```bash
562
+ # Clone the monorepo
563
+ git clone <repo-url>
564
+ cd flagmint-monorepo
565
+
566
+ # Install dependencies
567
+ pnpm install
568
+
569
+ # Build the library
570
+ pnpm --filter react-flagmint build
571
+
572
+ # Run the Next.js example
573
+ pnpm dev:next
574
+ ```
575
+
576
+ ### Testing
577
+
578
+ ```bash
579
+ # Run tests
580
+ pnpm test
581
+
582
+ # Type checking
583
+ pnpm typecheck
584
+
585
+ # Lint
586
+ pnpm lint
587
+ ```
588
+
589
+ ## 📄 License
590
+
591
+ MIT
592
+
593
+ ## 🤝 Contributing
594
+
595
+ 1. Fork the repository
596
+ 2. Create a feature branch
597
+ 3. Make your changes
598
+ 4. Add tests
599
+ 5. Submit a pull request
600
+
601
+ ## 📞 Support
602
+
603
+ - 📧 Email: support@flagmint.com
604
+ - 🐛 Issues: [GitHub Issues](https://github.com/jtad009/flagmint-react-sdk/issues)
605
+ - 📖 Docs: [Documentation](https://docs.flagmint.com)
606
+
607
+ ---
608
+
609
+ **Made with ❤️ by the Flagmint team**
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "flagmint-monorepo",
3
+ "version": "1.0.11",
4
+ "type": "module",
5
+ "scripts": {
6
+ "build": "pnpm --filter react-flagmint build",
7
+ "dev:next": "pnpm --filter nextjs-app dev",
8
+ "clean": "pnpm -r exec rm -rf dist node_modules"
9
+ },
10
+ "devDependencies": {
11
+ "typescript": "^5.0.0"
12
+ }
13
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "flagmint-react-sdk",
3
+ "version": "0.6.0",
4
+ "description": "React wrapper for flagmint-js-sdk",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.mjs",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.mjs",
14
+ "require": "./dist/index.cjs"
15
+ },
16
+ "./client": {
17
+ "types": "./dist/client.d.ts",
18
+ "import": "./dist/client.mjs"
19
+ },
20
+ "./package.json": "./package.json"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "prepublishOnly": "pnpm run build"
28
+ },
29
+ "peerDependencies": {
30
+ "react": ">=18.0.0",
31
+ "react-dom": ">=18.0.0"
32
+ },
33
+ "dependencies": {
34
+ "zustand": "^4.4.0",
35
+ "flagmint-js-sdk": "1.0.11"
36
+ },
37
+ "devDependencies": {
38
+ "tsup": "^8.0.0",
39
+ "typescript": "^5.0.0",
40
+ "@types/react": "^18.0.0",
41
+ "@types/react-dom": "^18.0.0"
42
+ }
43
+ }
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+ import { FlagValue } from "flagmint-js-sdk"
3
+ import { useContext } from "react"
4
+ import { StoreApi, useStore } from "zustand"
5
+ import { FlagmintStoreContext } from "@/providers/FlagmintProvider"
6
+ import { FlagStore, Flags } from "@/types"
7
+
8
+ // Helper to get the store from context
9
+ export function useFlagmintStore(): StoreApi<FlagStore> {
10
+ const store = useContext(FlagmintStoreContext)
11
+ if (!store) {
12
+ throw new Error('useFlagmintStore must be used within a FlagmintProvider')
13
+ }
14
+ return store
15
+ }
16
+
17
+ // Hook to get the client instance and update context
18
+ export function useFlagmint() {
19
+ const store = useFlagmintStore()
20
+ const client = useStore(store, (state) => state.client)
21
+ const isInitialized = useStore(store, (state) => state.isInitialized)
22
+
23
+ const updateContext = async (context: Record<string, any>) => {
24
+ if (!client) {
25
+ throw new Error('Flagmint client not initialized')
26
+ }
27
+ await client.updateContext(context)
28
+ }
29
+
30
+ return {
31
+ client,
32
+ isInitialized,
33
+ updateContext
34
+ }
35
+ }
36
+
37
+ // Hook to get all flags - only re-renders when flags change
38
+ export function useFlags(): Flags {
39
+ const store = useFlagmintStore()
40
+ return useStore(store, (state) => state.flags)
41
+ }
42
+
43
+ // Simplified useFlag - just read from the reactive store
44
+ export function useFlag<T = FlagValue>(
45
+ key: string,
46
+ fallback?: T
47
+ ): T {
48
+ const store = useFlagmintStore()
49
+
50
+ // Subscribe to this specific flag in the store
51
+ const flagValue = useStore(store, (state) => state.flags[key])
52
+
53
+ // Return the flag value or fallback
54
+ return (flagValue ?? fallback) as T
55
+ }
56
+
57
+ // Hook to check if the client is ready
58
+ export function useFlagmintReady(): boolean {
59
+ const store = useFlagmintStore()
60
+
61
+ // Subscribe to both isReady and isInitialized
62
+ const isReady = useStore(store, (state) => state.isReady)
63
+ const isInitialized = useStore(store, (state) => state.isInitialized)
64
+
65
+ return isReady && isInitialized
66
+ }
@@ -0,0 +1 @@
1
+ export * from './types';
@@ -0,0 +1,113 @@
1
+ 'use client';
2
+ import { FlagClient } from "flagmint-js-sdk"
3
+ import { useMemo, useRef, useEffect, createContext } from "react"
4
+ import { createFlagStore } from "@/store/store"
5
+ import { FlagmintProviderProps, Flags, FlagStore } from "@/types"
6
+ import { StoreApi } from "zustand"
7
+
8
+ export const FlagmintStoreContext = createContext<StoreApi<FlagStore> | null>(null)
9
+
10
+ /**
11
+ * Provides the Flagmint flag store to React components and keeps it synced with FlagClient updates.
12
+ * Lifecycle overview:
13
+ * - App loads → FlagClient constructed → initialize() begins.
14
+ * - Cached flags applied → subscribers notified → React renders cached values.
15
+ * - Transport set up → initial fetch → store updated → subscribers notified.
16
+ * - Subsequent server updates propagate via client.subscribe to re-render consumers.
17
+ * - When context changes (updateContext), flags are re-fetched and the store is refreshed.
18
+ *
19
+ * @param props Component props: children, client options, optional initial flags, and deferInitialization flag.
20
+ * @returns A provider that supplies the flag store context to its children.
21
+ */
22
+ export function FlagmintProvider<
23
+ T extends FlagClient<T, C>,
24
+ C extends Record<string, any> = Record<string, any>
25
+ >({
26
+ children,
27
+ options,
28
+ initialFlags = {},
29
+ deferInitialization = false
30
+ }: FlagmintProviderProps<T, C>) {
31
+ // Create a new store instance for each provider (per-request in SSR)
32
+ const store = useMemo(() => createFlagStore(initialFlags), [initialFlags])
33
+
34
+ // Use ref to avoid recreating client on every render
35
+ const clientRef = useRef<FlagClient<T, C> | null>(null)
36
+ const initPromiseRef = useRef<Promise<FlagClient<T, C>> | null>(null)
37
+ const unsubscribeRef = useRef<(() => void) | null>(null)
38
+
39
+ // Initialization function similar to Vue plugin
40
+ const init = async (): Promise<FlagClient<T, C>> => {
41
+ if (clientRef.current) return clientRef.current // Avoid re-init
42
+
43
+ // Return existing promise if init is already in progress
44
+ if (initPromiseRef.current) return initPromiseRef.current
45
+
46
+ const { setClient, setReady, setInitialized, setFlags } = store.getState()
47
+
48
+ initPromiseRef.current = (async () => {
49
+ try {
50
+ // Create the FlagClient
51
+ const client = new FlagClient<T, C>(options)
52
+
53
+ // Wait for client to be ready
54
+ await client.ready()
55
+
56
+ // Get initial flags from client
57
+ const initialClientFlags = client.getFlags()
58
+ setFlags(initialClientFlags)
59
+
60
+ // Subscribe to flag changes - THIS IS THE KEY FIX
61
+ const unsubscribe = client.subscribe((updatedFlags: Flags) => {
62
+ console.log('FlagmintProvider - flags updated:', updatedFlags)
63
+ setFlags(updatedFlags)
64
+ })
65
+
66
+ unsubscribeRef.current = unsubscribe
67
+
68
+ clientRef.current = client
69
+ setClient(client)
70
+ setReady(true)
71
+ setInitialized(true)
72
+
73
+ console.log('FlagmintProvider - initialized with flags:', initialClientFlags)
74
+
75
+ return client
76
+ } catch (error) {
77
+ console.error('Failed to initialize Flagmint client:', error)
78
+ throw error
79
+ }
80
+ })()
81
+
82
+ return initPromiseRef.current
83
+ }
84
+
85
+ useEffect(() => {
86
+ if (!deferInitialization) {
87
+ init().catch(error => {
88
+ console.error('Auto-initialization failed:', error)
89
+ })
90
+ }
91
+
92
+ return () => {
93
+ // Unsubscribe from flag changes
94
+ if (unsubscribeRef.current) {
95
+ unsubscribeRef.current()
96
+ unsubscribeRef.current = null
97
+ }
98
+
99
+ // Destroy client
100
+ if (clientRef.current) {
101
+ clientRef.current.destroy?.()
102
+ clientRef.current = null
103
+ initPromiseRef.current = null
104
+ }
105
+ }
106
+ }, [deferInitialization])
107
+
108
+ return (
109
+ <FlagmintStoreContext.Provider value={store}>
110
+ {children}
111
+ </FlagmintStoreContext.Provider>
112
+ )
113
+ }
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+ import { createStore, StoreApi } from "zustand/vanilla";
3
+ import { Flags, FlagStore } from "@/types";
4
+
5
+
6
+ export function createFlagStore(initialFlags: Flags = {}): StoreApi<FlagStore> {
7
+ return createStore<FlagStore>((set, get) => ({
8
+ flags: initialFlags,
9
+ client: null,
10
+ isInitialized: false,
11
+ isReady: false,
12
+ setFlags: (flags) => set({ flags }),
13
+ setFlag: (key, value) => set((state) => ({
14
+ flags: { ...state.flags, [key]: value }
15
+ })),
16
+ setClient: (client) => set({ client }),
17
+ setInitialized: (initialized) => set({ isInitialized: initialized }),
18
+ setReady: (ready) => set({ isReady: ready })
19
+ }))
20
+ }
@@ -0,0 +1,46 @@
1
+ import { FlagClient, FlagClientOptions } from "flagmint-js-sdk"
2
+ import { ReactNode } from "react"
3
+
4
+ export type FlagValue = string | number | boolean | object | null
5
+
6
+ export interface Flags {
7
+ [key: string]: FlagValue
8
+ }
9
+
10
+ export interface CoreClientLike {
11
+ getFlags(): Promise<Flags> | Flags
12
+ getFlag(key: string, fallback?: FlagValue): Promise<FlagValue> | FlagValue
13
+ subscribe(callback: (flags: Flags) => void): () => void
14
+ updateContext(context: Record<string, any>): Promise<void> | void
15
+ init(): Promise<void> | void
16
+ close(): Promise<void> | void
17
+ }
18
+
19
+ export interface FlagmintProviderProps {
20
+ children: ReactNode
21
+ createClient?: () => CoreClientLike
22
+ client: CoreClientLike
23
+ initialFlags?: Flags
24
+ }
25
+
26
+ export interface FlagStore {
27
+ flags: Flags
28
+ client: FlagClient<any, any> | null
29
+ isInitialized: boolean
30
+ isReady: boolean
31
+ setFlags: (flags: Flags) => void
32
+ setFlag: (key: string, value: FlagValue) => void
33
+ setClient: (client: FlagClient<any, any> | null) => void
34
+ setInitialized: (initialized: boolean) => void
35
+ setReady: (ready: boolean) => void
36
+ }
37
+
38
+ export interface FlagmintProviderProps<
39
+ T = unknown,
40
+ C extends Record<string, any> = Record<string, any>
41
+ > {
42
+ children: ReactNode
43
+ options: FlagClientOptions<C>
44
+ initialFlags?: Flags
45
+ deferInitialization?: boolean
46
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "declaration": true,
6
+ "noEmit": false,
7
+ "jsx": "react-jsx",
8
+ "incremental": false,
9
+ "baseUrl": ".",
10
+ "paths": {
11
+ "@/*": ["src/*"]
12
+ }
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["dist", "node_modules"]
16
+ }
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: {
5
+ index: 'src/index.ts',
6
+ },
7
+ format: ['esm', 'cjs'],
8
+ dts: true,
9
+ sourcemap: true,
10
+ clean: true,
11
+ treeshake: true,
12
+ target: 'es2019',
13
+ external: ['react', 'react-dom', 'zustand', 'flagmint-js-sdk'],
14
+ esbuildOptions(options) {
15
+ options.jsx = 'automatic'
16
+ }
17
+ })
@@ -0,0 +1,3 @@
1
+ packages:
2
+ - 'packages/*'
3
+ - 'examples/*'
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2019",
4
+ "lib": ["dom", "dom.iterable", "es6"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ]
21
+ },
22
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
23
+ "exclude": ["node_modules", "dist", ".next"]
24
+ }