Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,66 @@ function FeatureList() {
}
```

### SupaFeature Component

A declarative component for rendering different UI based on boolean feature flag values.

```tsx
<SupaFeature
feature="feature-name"
loading={<Skeleton />}
variations={{
true: <ComponentA />,
false: <ComponentB />,
}}
/>
```

**Props:**

| Prop | Type | Required | Description |
| ------------- | --------------------------------------- | -------- | ---------------------------------------------------------------- |
| `feature` | `string` | Yes | The feature flag key to evaluate |
| `variations` | `{ true: ReactNode, false: ReactNode }` | Yes | Components to render for true/false values |
| `loading` | `ReactNode` | No | Component to render while loading |
| `fallback` | `ReactNode` | No | Component to render when feature value is neither true nor false |
| `context` | `Record<string, unknown>` | No | Context override for this feature |
| `shouldFetch` | `boolean` | No | Whether to fetch the feature (default: true) |

**Examples:**

```tsx
// Simple boolean feature flag
function Header() {
return (
<SupaFeature
feature="new-header"
loading={<HeaderSkeleton />}
variations={{
true: <NewHeader />,
false: <OldHeader />,
}}
/>
)
}

// With context
function UserDashboard() {
const { user } = useAuth()

return (
<SupaFeature
feature="beta-dashboard"
context={{ userId: user.id, plan: user.plan }}
variations={{
true: <BetaDashboard />,
false: <StandardDashboard />,
}}
/>
)
}
```

### useFeatureContext Hook

Access and update the feature context within components.
Expand Down
41 changes: 19 additions & 22 deletions packages/react/src/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@ export interface SupaFeatureProps {
shouldFetch?: boolean

/**
* Variations object mapping feature values/keys to JSX elements
* Variations object mapping "true" or "false" to JSX elements
*/
variations: Record<string, ReactNode>
variations: {
true: ReactNode
false: ReactNode
}

/**
* Key in variations object to use for loading state
* Component to render during loading state
*/
loading?: string
loading?: ReactNode
}

/**
Expand All @@ -40,10 +43,10 @@ export interface SupaFeatureProps {
* ```tsx
* <SupaFeature
* feature="new-header"
* loading={<HeaderSkeleton />}
* variations={{
* "true": <NewHeader />,
* "false": <OldHeader />,
* loading: <HeaderSkeleton />
* true: <NewHeader />,
* false: <OldHeader />
* }}
* />
* ```
Expand All @@ -61,26 +64,20 @@ export function SupaFeature({
})

// Show loading state if provided and currently loading
if (isLoading && loading && variations[loading]) {
return <>{variations[loading]}</>
}

// Don't render anything if still loading and no loader provided
if (isLoading) {
return null
return loading ? <>{loading}</> : null
}

// Don't render anything if no feature value (client config should provide defaults)
if (!hasValue(featureValue)) {
return null
}

// Convert feature value to string for object key lookup
// Convert feature value to boolean string for lookup
const valueKey = String(featureValue)

// Find matching variation by exact key match
if (variations[valueKey]) {
return <>{variations[valueKey]}</>
// Match "true" or "false" variations
if (variations.true && valueKey === 'true') {
return <>{variations.true}</>
}

if (variations.false && (valueKey === 'false' || !hasValue(featureValue))) {
return <>{variations.false}</>
}

// Don't render anything if no variation matches
Expand Down
Loading