npm package: @flagpool/react
Description: Official Flagpool React SDK - React hooks and components for feature flags
Author: Flagpool
Latest version: 0.3.6

# @flagpool/react

Official React SDK for [Flagpool](https://flagpool.io) - React hooks and components for feature flags.

[![npm version](https://badge.fury.io/js/@flagpool%2Freact.svg)](https://www.npmjs.com/package/@flagpool/react)

## Installation

```bash
npm install @flagpool/react
```

## Quick Start

### 1. Wrap your app with the Provider

```tsx
import { FlagpoolProvider } from '@flagpool/react';

function App() {
  return (
    <FlagpoolProvider
      projectId="your-project-uuid"
      apiKey="your-api-key"
      decryptionKey="your-decryption-key"
      context={{ userId: 'user-123', plan: 'pro' }}
    >
      <MyApp />
    </FlagpoolProvider>
  );
}
```

### 2. Use hooks in your components

```tsx
import { useFlag, useFlagValue } from '@flagpool/react';

function MyComponent() {
  // Boolean flag
  const showNewFeature = useFlag('new-feature');
  
  // String/number/object flag
  const buttonColor = useFlagValue('cta-button-color', 'blue');
  
  return (
    <div>
      {showNewFeature && <NewFeature />}
      <Button color={buttonColor}>Click me</Button>
    </div>
  );
}
```

## API Reference

### Provider

#### `<FlagpoolProvider>`

Provides feature flag context to your React app.

```tsx
<FlagpoolProvider
  projectId="uuid-xxx"            // Required: Project ID from dashboard
  apiKey="pk_live_xxx"            // Required: Environment-specific API key
  decryptionKey="fp_dec_xxx"      // Required: For CDN URL hash & target lists
  context={{ userId: 'u1' }}      // Optional: User context for targeting
  cryptoAdapter={adapter}         // Optional: For decrypting target lists
  analytics={{ enabled: true }}   // Optional: Enable analytics (paid plans)
  pollingInterval={30000}         // Optional: Polling interval in ms
  streaming={true}                // Optional: Enable real-time updates
  urlOverride="https://..."       // Optional: Complete URL override
  onReady={() => {}}              // Optional: Called when flags are ready
  onError={(err) => {}}           // Optional: Called on error
>
  <App />
</FlagpoolProvider>
```

### Hooks

#### `useFlag(flagKey, defaultValue?)`

Get a boolean feature flag value.

```tsx
const isEnabled = useFlag('new-feature');
const isEnabled = useFlag('new-feature', false); // with default
```

#### `useFlagValue<T>(flagKey, defaultValue?)`

Get a flag value of any type.

```tsx
const color = useFlagValue('button-color', 'blue');
const limit = useFlagValue<number>('rate-limit', 100);
const config = useFlagValue<Config>('feature-config', defaultConfig);
```

#### `useFlagDetails<T>(flagKey, defaultValue?)`

Get detailed flag information including loading state.

```tsx
const { value, isReady, isLoading, error } = useFlagDetails('feature', false);

if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return value ? <NewFeature /> : <OldFeature />;
```

#### `useFlagpool()`

Access the Flagpool client and context directly.

```tsx
const { client, isReady, isLoading, error, setContext } = useFlagpool();

// Update context after login
useEffect(() => {
  setContext({ userId: user.id, plan: user.plan });
}, [user]);
```

### Components

#### `<Feature>`

Conditionally render content based on a boolean flag.

```tsx
// Simple usage
<Feature flag="new-checkout">
  <NewCheckout />
</Feature>

// With fallback
<Feature flag="new-checkout" fallback={<OldCheckout />}>
  <NewCheckout />
</Feature>
```

#### `<Variant>`

Render content when a flag matches a specific value. Perfect for A/B tests.

```tsx
<Variant flag="cta-color" value="blue">
  <BlueButton />
</Variant>
<Variant flag="cta-color" value="green">
  <GreenButton />
</Variant>
<Variant flag="cta-color" value="red">
  <RedButton />
</Variant>
```

#### `<FeatureWithLoading>`

Feature component with loading and error states.

```tsx
<FeatureWithLoading
  flag="new-checkout"
  loading={<Spinner />}
  error={(err) => <ErrorMessage message={err.message} />}
  fallback={<OldCheckout />}
>
  <NewCheckout />
</FeatureWithLoading>
```

## TypeScript Support

Full TypeScript support is included. Use generics for type-safe flag values:

```tsx
interface FeatureConfig {
  maxItems: number;
  theme: 'light' | 'dark';
}

const config = useFlagValue<FeatureConfig>('feature-config', {
  maxItems: 10,
  theme: 'light',
});
```

## Server-Side Rendering (SSR)

For SSR frameworks like Next.js, the provider handles hydration automatically. Flags will show their default values during server render and update on the client once initialized.

For optimal SSR support, consider using `useFlagDetails` to handle loading states:

```tsx
function MyComponent() {
  const { value, isLoading } = useFlagDetails('feature', false);
  
  // During SSR and initial client render
  if (isLoading) {
    return <DefaultView />;
  }
  
  return value ? <NewFeature /> : <OldFeature />;
}
```

## Analytics (Paid Plans Only)

Track flag evaluation counts to understand usage patterns. Analytics is **opt-in** and disabled by default.

### Enabling Analytics

```tsx
<FlagpoolProvider
  projectId="your-project-uuid"
  apiKey="your-api-key"
  decryptionKey="your-decryption-key"
  analytics={{ enabled: true }}
>
  <App />
</FlagpoolProvider>
```

### Configuration Options

```tsx
<FlagpoolProvider
  // ...required props
  analytics={{
    enabled: true,
    flushInterval: 60000,     // Flush interval in ms (min: 30000)
    flushThreshold: 100,      // Flush after N evaluations
    sampleRate: 1.0,          // Sample rate (0.0 - 1.0)
  }}
>
  <App />
</FlagpoolProvider>
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `enabled` | boolean | `false` | Enable/disable analytics |
| `flushInterval` | number | `60000` | Flush interval in ms (minimum: 30000) |
| `flushThreshold` | number | `100` | Flush after N evaluations |
| `sampleRate` | number | `1.0` | Sample rate (0.0 - 1.0) |

### How It Works

- Evaluation counts are batched in memory
- Batches are sent asynchronously (fire-and-forget)
- Analytics never blocks flag evaluation or re-renders
- Data is aggregated daily in your Flagpool dashboard
- **Note**: Data is silently discarded for free plan projects

### Debugging Analytics

Access analytics state via the `useFlagpool` hook:

```tsx
function DebugPanel() {
  const { client } = useFlagpool();
  
  const analyticsState = client?.getAnalyticsState();
  const flagsWithState = client?.getAllFlagsWithState();
  
  console.log('Buffer:', analyticsState?.buffer);
  console.log('Flags:', flagsWithState);
  
  return null;
}
```

Or use the browser console:

```javascript
// Global debug object (browser only)
console.log(__FLAGPOOL__.state.analytics);
console.log(__FLAGPOOL__.state.flags);
__FLAGPOOL__.flushAnalytics();
```

## Target List Encryption

By default, target lists (used for `inTargetList` and `notInTargetList` operators) are encrypted in SDK exports to protect sensitive user data like emails and user IDs.

### Using Encrypted Target Lists

If your environment has target list encryption enabled (the default), you need to provide a crypto adapter. The easiest way is to pass it directly to the `FlagpoolProvider`:

```tsx
import { FlagpoolProvider, CryptoAdapter } from '@flagpool/react';

// Web Crypto adapter for browsers
const webCryptoAdapter: CryptoAdapter = {
  async decrypt(ciphertext, key, iv, tag) {
    const keyBase64 = key.startsWith('tlk_') ? key.slice(4) : key;
    const keyBytes = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
    const ivBytes = Uint8Array.from(atob(iv), c => c.charCodeAt(0));
    const ciphertextBytes = Uint8Array.from(atob(ciphertext), c => c.charCodeAt(0));
    const tagBytes = Uint8Array.from(atob(tag), c => c.charCodeAt(0));

    const cryptoKey = await crypto.subtle.importKey(
      'raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']
    );

    const combined = new Uint8Array(ciphertextBytes.length + tagBytes.length);
    combined.set(ciphertextBytes, 0);
    combined.set(tagBytes, ciphertextBytes.length);

    const decrypted = await crypto.subtle.decrypt(
      { name: 'AES-GCM', iv: ivBytes }, cryptoKey, combined
    );

    return new TextDecoder().decode(decrypted);
  }
};

function App() {
  return (
    <FlagpoolProvider
      projectId="your-project-uuid"
      apiKey="your-api-key"
      decryptionKey="tlk_xxx..."
      cryptoAdapter={webCryptoAdapter}
      context={{ userId: 'user-123' }}
    >
      <MyApp />
    </FlagpoolProvider>
  );
}
```

### Error: "Encrypted target lists received but no crypto adapter configured"

If you see this error, it means:

1. **Your environment has target list encryption enabled** (the default)
2. **You haven't provided a crypto adapter**

**Solutions:**

1. **Provide a crypto adapter** (recommended for production):
   ```tsx
   <FlagpoolProvider
     cryptoAdapter={webCryptoAdapter}
     // ... other props
   >
   ```

2. **Disable encryption** (for development/testing):
   - Go to Flagpool Dashboard → Settings → Environments
   - Toggle off "Target List Encryption" for your environment

> ⚠️ **Security Note:** Disabling encryption exposes target list values (emails, user IDs, etc.) in plaintext. Only disable for development or non-sensitive data.

## License

MIT © [Flagpool](https://flagpool.io)