npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-feature-flagkit

v1.0.1

Published

Lightweight, type-safe feature flag system for React and React Native. Zero runtime deps, offline-first, remote config sync, and A/B test primitives.

Readme

react-feature-flagkit

npm version License: MIT TypeScript Zero Dependencies React

A lightweight, type-safe feature flag system for React and React Native.
Zero runtime dependencies. Offline-first. Remote config sync. A/B test primitives built in.


Features

| Feature | Details | |---|---| | Zero runtime deps | Only peer dep is React ≥ 18 | | Type-safe | Full TypeScript generics — useFlag<string>('theme', 'light') | | Offline-first | Pluggable storage adapter; restores from cache before network | | Remote sync | Fetch + optional polling from any URL | | Targeting | 11 operators: eq, neq, in, nin, gt, gte, lt, lte, contains, startsWith, endsWith | | Gradual rollout | Deterministic FNV-1a hash — same user always in/out | | A/B variants | Weighted variant assignment, stable per user | | Local overrides | setOverride() for DevTools, Storybook, and testing | | Auto-expiry | expiresAt timestamp — flag disables itself | | React Native | Works with AsyncStorage/MMKV via StorageAdapter interface |


Installation

npm install react-feature-flagkit
# or
yarn add react-feature-flagkit
# or
pnpm add react-feature-flagkit

Peer dependency: React ≥ 18.0.0


Quick Start

import { FeatureFlagProvider, useFlag, FeatureFlag } from 'react-feature-flagkit';

const flags = [
  { key: 'new-dashboard', defaultValue: false, enabled: true },
  { key: 'ui-theme',      defaultValue: 'light', enabled: true },
];

function App() {
  return (
    <FeatureFlagProvider flags={flags} user={{ id: 'user-123' }}>
      <MyApp />
    </FeatureFlagProvider>
  );
}

// Hook usage
function Header() {
  const theme = useFlag('ui-theme', 'light');         // → 'light' | 'dark'
  const isDashboardEnabled = useFlag('new-dashboard', false); // → boolean
  return <header className={theme}>{...}</header>;
}

// Component usage
function Home() {
  return (
    <FeatureFlag name="new-dashboard" fallback={<OldDashboard />}>
      <NewDashboard />
    </FeatureFlag>
  );
}

API Reference

<FeatureFlagProvider>

Wraps your app. All configuration lives here.

<FeatureFlagProvider
  flags={staticFlags}          // FlagDefinition[] — static flags
  remote={remoteConfig}        // RemoteConfig — fetch from URL
  user={{ id: 'u1', attributes: { plan: 'pro', country: 'IN' } }}
  overrides={{ 'debug-mode': true }}  // local overrides (highest priority)
  storage={storageAdapter}     // StorageAdapter — for persistence
  fallbackFlags={fallbackFlags} // used when remote fails and no cache
  debug={true}                 // logs evaluations to console
>
  {children}
</FeatureFlagProvider>

Hooks

useFlag(key, defaultValue)T

Evaluate a flag. Returns the resolved value.

const isEnabled = useFlag('new-feature', false);
const theme = useFlag<'light' | 'dark'>('theme', 'light');
const limit = useFlag('rate-limit', 100);

useFlagResult(key, defaultValue)EvaluationResult<T>

Like useFlag but returns the full result including reason and variant key.

const { value, reason, isEnabled, variantKey } = useFlagResult('ab-test', 'control');
// reason: 'VARIANT' | 'TARGETING' | 'ROLLOUT' | 'DEFAULT' | 'OVERRIDE' | 'EXPIRED' | 'ENABLED'

useFlags(keysMap)Record<K, V>

Evaluate multiple flags in one call.

const { 'new-ui': newUi, 'dark-mode': darkMode } = useFlags({
  'new-ui': false,
  'dark-mode': false,
  'api-version': 'v1',
});

useFlagVariant(key)string | null

Get the active A/B variant key, or null if no variant is active.

const variant = useFlagVariant('checkout-experiment');
// → 'control' | 'treatment-a' | 'treatment-b' | null

useFeatureFlagOverride(key)

Programmatic overrides — great for DevTools and Storybook.

const { enable, disable, set, remove } = useFeatureFlagOverride('new-feature');
<button onClick={enable}>Enable</button>
<button onClick={disable}>Disable</button>
<button onClick={() => set('dark')}>Set Dark</button>

useFlagStatus()

Loading / error / freshness state.

const { isLoading, error, lastUpdated } = useFlagStatus();
if (isLoading) return <Spinner />;

useSetUser()

Update user context at runtime (e.g. after login).

const setUser = useSetUser();
// After login:
setUser({ id: user.id, attributes: { plan: user.plan, country: user.country } });

useRefreshFlags()

Manually trigger a remote refresh.

const refresh = useRefreshFlags();
<button onClick={refresh}>Reload Flags</button>

<FeatureFlag> Component

// Basic
<FeatureFlag name="new-checkout">
  <NewCheckout />
</FeatureFlag>

// With fallback
<FeatureFlag name="new-checkout" fallback={<OldCheckout />}>
  <NewCheckout />
</FeatureFlag>

// Match specific value (for config flags)
<FeatureFlag name="ui-theme" whenValue="dark">
  <DarkModeStyles />
</FeatureFlag>

withFeatureFlag(Component, options) HOC

const ProtectedPage = withFeatureFlag(AdminPage, {
  flagKey: 'admin-access',
  fallback: AccessDeniedPage,
});

Flag Definition

interface FlagDefinition<T = FlagValue> {
  key: string;              // unique identifier
  defaultValue: T;          // value when disabled / not matched
  enabled: boolean;         // master on/off switch

  // Optional
  description?: string;
  expiresAt?: number;       // Unix ms — auto-disables after this time
  rollout?: {
    percentage: number;     // 0-100, deterministic per userId
    seed?: string;          // custom hash seed (defaults to flag key)
  };
  targeting?: TargetingRule[];  // ALL rules must match
  variants?: Variant<T>[];      // A/B test variants
  metadata?: Record<string, unknown>;
}

Recipes

Remote config with polling

<FeatureFlagProvider
  remote={{
    url: 'https://your-api.com/flags',
    pollingInterval: 60_000,           // refresh every 60s
    headers: { Authorization: `Bearer ${token}` },
    timeout: 5000,
    onError: (err) => console.error('Flag fetch failed', err),
  }}
>
  {children}
</FeatureFlagProvider>

Gradual rollout (10% → 50% → 100%)

const flags = [{
  key: 'new-checkout',
  defaultValue: false,
  enabled: true,
  rollout: { percentage: 10 },  // bump to 50, then 100 over time
}];

Targeting: show only to Pro users in India

const flags = [{
  key: 'beta-export',
  defaultValue: false,
  enabled: true,
  targeting: [
    { attribute: 'plan',    operator: 'eq', value: 'pro' },
    { attribute: 'country', operator: 'eq', value: 'IN'  },
  ],
}];

A/B test with weighted variants

const flags = [{
  key: 'checkout-button',
  defaultValue: 'blue',
  enabled: true,
  variants: [
    { key: 'control',     value: 'blue',  weight: 50 },
    { key: 'treatment-a', value: 'green', weight: 30 },
    { key: 'treatment-b', value: 'red',   weight: 20 },
  ],
}];

function CheckoutButton() {
  const color = useFlag('checkout-button', 'blue');
  const variant = useFlagVariant('checkout-button');
  // Track exposure: analytics.track('flag_exposure', { variant })
  return <Button color={color} />;
}

Persist flags with localStorage (web)

<FeatureFlagProvider
  remote={{ url: '/api/flags' }}
  storage={{
    getItem: (k) => localStorage.getItem(k),
    setItem: (k, v) => localStorage.setItem(k, v),
    removeItem: (k) => localStorage.removeItem(k),
  }}
>

React Native with AsyncStorage

import AsyncStorage from '@react-native-async-storage/async-storage';

<FeatureFlagProvider
  remote={{ url: 'https://your-api.com/flags' }}
  storage={AsyncStorage}   // AsyncStorage matches StorageAdapter interface
>

React Native with MMKV (faster)

import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();

<FeatureFlagProvider
  storage={{
    getItem:    (k) => storage.getString(k) ?? null,
    setItem:    (k, v) => storage.set(k, v),
    removeItem: (k) => storage.delete(k),
  }}
>

Auto-expiring launch flag

const flags = [{
  key: 'black-friday-banner',
  defaultValue: false,
  enabled: true,
  expiresAt: new Date('2025-11-30T23:59:59Z').getTime(),
}];
// After Nov 30, evaluates to { reason: 'EXPIRED', isEnabled: false }

Override in tests

import { FeatureFlagProvider } from 'react-feature-flagkit';

function renderWithFlags(ui, overrides = {}) {
  return render(
    <FeatureFlagProvider flags={[]} overrides={overrides}>
      {ui}
    </FeatureFlagProvider>
  );
}

test('shows new dashboard when flag enabled', () => {
  renderWithFlags(<Home />, { 'new-dashboard': true });
  expect(screen.getByTestId('new-dashboard')).toBeInTheDocument();
});

Evaluation Priority

Flags are evaluated in this order (highest wins):

1. Local override    → reason: 'OVERRIDE'
2. Flag not found    → reason: 'DEFAULT'
3. Flag expired      → reason: 'EXPIRED'
4. Flag disabled     → reason: 'DEFAULT'
5. A/B variants      → reason: 'VARIANT'
6. Targeting rules   → reason: 'TARGETING' | 'DEFAULT'
7. Rollout %         → reason: 'ROLLOUT' | 'ROLLOUT_EXCLUDED'
8. Simple enabled    → reason: 'ENABLED'

Remote API Response Format

The default transform expects an array of FlagDefinition objects:

[
  { "key": "new-dashboard", "defaultValue": false, "enabled": true },
  { "key": "ui-theme", "defaultValue": "light", "enabled": true, "rollout": { "percentage": 50 } }
]

For a different shape, provide a transform function:

remote={{
  url: '/api/flags',
  transform: (raw) => raw.data.featureFlags.map(f => ({
    key: f.name,
    defaultValue: f.fallback,
    enabled: f.active,
  })),
}}

TypeScript

All APIs are fully typed. Generics flow through:

const theme = useFlag<'light' | 'dark'>('ui-theme', 'light');
// theme: 'light' | 'dark'  ✓

const { value } = useFlagResult<number>('rate-limit', 100);
// value: number  ✓

Contributing

git clone https://github.com/your-username/react-feature-flagkit
cd react-feature-flagkit
npm install
node run-tests.js   # run test suite (zero deps, uses node:test)

License

MIT