Skip to content

Commit cf44249

Browse files
committed
feat: better feature flags & props
1 parent 45f6558 commit cf44249

File tree

6 files changed

+114
-48
lines changed

6 files changed

+114
-48
lines changed

apps/dashboard/app/(main)/websites/[id]/flags/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export default function FlagsPage() {
5959

6060
const { data: website } = useWebsite(websiteId);
6161
const { isEnabled } = useFlags();
62-
const isExperimentEnabled = isEnabled('experiment-50');
62+
const experimentFlag = isEnabled('experiment-50');
6363

6464
const {
6565
data: flags,
@@ -144,15 +144,15 @@ export default function FlagsPage() {
144144
websiteId={websiteId}
145145
websiteName={website?.name || undefined}
146146
/>
147-
{isExperimentEnabled !== undefined && (
147+
{experimentFlag.isReady && (
148148
<div className="flex items-center gap-2">
149149
<FlagIcon
150150
className="h-5 w-5"
151-
color={isExperimentEnabled ? 'red' : 'blue'}
151+
color={experimentFlag.enabled ? 'red' : 'blue'}
152152
size={16}
153153
weight="fill"
154154
/>
155-
{isExperimentEnabled ? (
155+
{experimentFlag.enabled ? (
156156
<Badge className="bg-red-500 text-white">Red Team</Badge>
157157
) : (
158158
<Badge className="bg-blue-500 text-white">Blue Team</Badge>

apps/docs/content/docs/features/feature-flags.mdx

Lines changed: 83 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,15 @@ function App() {
4949
}
5050
5151
function MyComponent() {
52-
const { isEnabled, getValue } = useFlags();
52+
const { isEnabled } = useFlags();
5353
54-
const showNewFeature = isEnabled('new-dashboard');
55-
const showBetaFeatures = isEnabled('beta-features');
56-
const darkModeEnabled = getValue('dark-mode-default', false);
54+
const newDashboardFlag = isEnabled('new-dashboard');
55+
const betaFeaturesFlag = isEnabled('beta-features');
5756
5857
return (
59-
<div className={darkModeEnabled ? 'dark' : ''}>
60-
{showNewFeature !== undefined && showNewFeature && <NewDashboard />}
61-
{showBetaFeatures !== undefined && showBetaFeatures && <BetaFeatures />}
58+
<div>
59+
{newDashboardFlag.isReady && newDashboardFlag.enabled && <NewDashboard />}
60+
{betaFeaturesFlag.isReady && betaFeaturesFlag.enabled && <BetaFeatures />}
6261
<button>Click me</button>
6362
</div>
6463
);
@@ -90,11 +89,11 @@ function App() {
9089
function MyComponent() {
9190
const { isEnabled } = useFlags();
9291
93-
const showNewFeature = isEnabled('new-feature');
92+
const newFeatureFlag = isEnabled('new-feature');
9493
9594
return (
9695
<div>
97-
{showNewFeature !== undefined && showNewFeature && <NewFeature />}
96+
{newFeatureFlag.isReady && newFeatureFlag.enabled && <NewFeature />}
9897
<button>Click me</button>
9998
</div>
10099
);
@@ -162,25 +161,55 @@ const showNewUI = isEnabled('new-ui-rollout');`}
162161
<CodeBlock language="tsx">
163162
{`const {
164163
isEnabled,
165-
getValue,
166164
fetchAllFlags,
167165
updateUser,
168166
refresh
169167
} = useFlags();
170168
171-
// Check if flag is enabled (returns boolean | undefined)
172-
const showFeature = isEnabled('my-feature'); // undefined if not evaluated yet
173-
const darkModeDefault = getValue('dark-mode', false);
169+
// Get flag state with loading information
170+
const featureFlag = isEnabled('my-feature');
171+
// Returns: { enabled: boolean, isLoading: boolean, isReady: boolean }
172+
173+
// Conditional rendering - clean and predictable
174+
{featureFlag.isReady && featureFlag.enabled && <NewFeature />}
174175
175-
// Only render when flag is evaluated
176-
{showFeature !== undefined && (
177-
showFeature ? <NewFeature /> : <OldFeature />
178-
)}
176+
// Show loading states explicitly
177+
{featureFlag.isLoading && <LoadingSpinner />}
178+
{!featureFlag.isReady && <Skeleton />}
179179
180180
// Refresh all flags
181181
await fetchAllFlags();`}
182182
</CodeBlock>
183183

184+
## Flag States
185+
186+
The `isEnabled` function returns a flag state object with loading information:
187+
188+
<CodeBlock language="tsx">
189+
{`const { isEnabled } = useFlags();
190+
const myFlag = isEnabled('my-feature');
191+
192+
// Always returns an object with these properties:
193+
myFlag.enabled // boolean - the flag value
194+
myFlag.isReady // boolean - true when flag has been evaluated
195+
myFlag.isLoading // boolean - true while fetching from server
196+
197+
// Clean, predictable conditional rendering
198+
{myFlag.isReady && myFlag.enabled && <MyFeature />}
199+
200+
// Show loading states when needed
201+
{myFlag.isLoading && <LoadingSpinner />}
202+
{!myFlag.isReady && <Skeleton />}
203+
{myFlag.isReady && myFlag.enabled && <Feature />}`}
204+
</CodeBlock>
205+
206+
**Benefits:**
207+
- No `undefined` checks required
208+
- Explicit loading states
209+
- More predictable behavior
210+
- Better TypeScript support
211+
- Cleaner component code
212+
184213
## Why isPending Matters
185214

186215
The `isPending` prop is crucial for preventing race conditions and ensuring consistent user experiences:
@@ -246,29 +275,55 @@ Enable debug mode to see flag evaluation in the browser console.
246275

247276
## Performance Benefits
248277

249-
- **Client-side Caching**: Flags are cached in IndexedDB and localStorage for instant loading
278+
- **Client-side Caching**: Flags are cached in localStorage for instant loading
250279
- **Intelligent Updates**: Only re-evaluates flags when user context changes
251280
- **Background Sync**: Fetches flag updates without blocking the UI
252281
- **Minimal Bundle Size**: Lightweight SDK with zero external dependencies
253282

254283
## Best Practices
255284

256-
1. **Use `isPending`** with authentication to prevent race conditions
257-
2. **Pass Custom Properties** for granular targeting and A/B testing
258-
3. **Enable Debug Mode** during development to monitor flag evaluation
259-
4. **Handle Flag Evaluation States** properly - flags return `undefined` until evaluated
260-
5. **Update User Context** after profile changes to refresh flag evaluation
285+
1. **Use `isEnabled` for flag states** - returns loading and ready information
286+
2. **Use `isPending`** with authentication to prevent race conditions
287+
3. **Pass Custom Properties** for granular targeting and A/B testing
288+
4. **Enable Debug Mode** during development to monitor flag evaluation
289+
5. **Check `isReady`** before showing features to avoid flash of incorrect content
290+
6. **Update User Context** after profile changes to refresh flag evaluation
261291

262292
<CodeBlock language="tsx">
263-
{`// Best practice: Wait for flag evaluation before rendering
293+
{`// ✅ Recommended: Check isReady before rendering features
264294
function FeatureComponent() {
265295
const { isEnabled } = useFlags();
266-
const showNewFeature = isEnabled('new-feature');
296+
const featureFlag = isEnabled('new-feature');
267297
268-
// Don't render until flag is evaluated
269-
if (showNewFeature === undefined) return <Skeleton />;
298+
// Always check isReady to avoid flash of wrong content
299+
if (!featureFlag.isReady) return <Skeleton />;
300+
return featureFlag.enabled ? <NewFeature /> : <OldFeature />;
301+
}
302+
303+
// ✅ Show loading states explicitly
304+
function FeatureWithLoading() {
305+
const { isEnabled } = useFlags();
306+
const featureFlag = isEnabled('new-feature');
270307
271-
return showNewFeature ? <NewFeature /> : <OldFeature />;
308+
if (featureFlag.isLoading) return <LoadingSpinner />;
309+
if (!featureFlag.isReady) return <Skeleton />;
310+
return featureFlag.enabled ? <NewFeature /> : <OldFeature />;
311+
}
312+
313+
// ✅ Multiple flags in one component
314+
function Dashboard() {
315+
const { isEnabled } = useFlags();
316+
const darkMode = isEnabled('dark-mode');
317+
const newLayout = isEnabled('new-layout');
318+
319+
// Both flags must be ready before rendering
320+
if (!darkMode.isReady || !newLayout.isReady) return <Skeleton />;
321+
322+
return (
323+
<div className={darkMode.enabled ? 'dark' : ''}>
324+
{newLayout.enabled ? <NewLayout /> : <OldLayout />}
325+
</div>
326+
);
272327
}`}
273328
</CodeBlock>
274329

packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@databuddy/sdk",
3-
"version": "2.1.6",
3+
"version": "2.1.7",
44
"description": "Official Databuddy Analytics SDK",
55
"main": "./dist/core/index.mjs",
66
"types": "./dist/core/index.d.ts",

packages/sdk/src/react/flags/flags-provider.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { atom, createStore, Provider, useAtom } from 'jotai';
22
import type { ReactNode } from 'react';
33
import { createElement, useEffect } from 'react';
44
import { flagStorage } from './flag-storage';
5-
import type { FlagResult, FlagsConfig } from './types';
5+
import type { FlagResult, FlagState, FlagsConfig } from './types';
66

77
const flagsStore = createStore();
88

@@ -318,20 +318,27 @@ export function useFlags() {
318318
return fetchFlag(key);
319319
};
320320

321-
const isEnabled = (key: string): boolean | undefined => {
321+
const isEnabled = (key: string): FlagState => {
322322
if (memoryFlags[key]) {
323-
return memoryFlags[key].enabled;
323+
return {
324+
enabled: memoryFlags[key].enabled,
325+
isLoading: false,
326+
isReady: true,
327+
};
324328
}
325-
getFlag(key);
326-
return;
327-
};
328-
329-
const getValue = (key: string, defaultValue = false): boolean => {
330-
if (memoryFlags[key]) {
331-
return memoryFlags[key].value ?? defaultValue;
329+
if (pendingFlags.has(key)) {
330+
return {
331+
enabled: false,
332+
isLoading: true,
333+
isReady: false,
334+
};
332335
}
333336
getFlag(key);
334-
return defaultValue;
337+
return {
338+
enabled: false,
339+
isLoading: true,
340+
isReady: false,
341+
};
335342
};
336343

337344
const refresh = async (forceClear = false): Promise<void> => {
@@ -382,7 +389,6 @@ export function useFlags() {
382389

383390
return {
384391
isEnabled,
385-
getValue,
386392
fetchAllFlags,
387393
updateUser,
388394
refresh,

packages/sdk/src/react/flags/types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,14 @@ export interface FlagsConfig {
2727
autoFetch?: boolean;
2828
}
2929

30+
export interface FlagState {
31+
enabled: boolean;
32+
isLoading: boolean;
33+
isReady: boolean;
34+
}
35+
3036
export interface FlagsContext {
31-
isEnabled: (key: string) => boolean | undefined;
32-
getValue: (key: string, defaultValue?: boolean) => boolean;
37+
isEnabled: (key: string) => FlagState;
3338
fetchAllFlags: () => Promise<void>;
3439
updateUser: (user: FlagsConfig['user']) => void;
3540
refresh: (forceClear?: boolean) => Promise<void>;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export { useFlags } from './flags-provider';
2-
export type { FlagsContext } from './types';
2+
export type { FlagState, FlagsContext } from './types';

0 commit comments

Comments
 (0)