@vortos/flags
v1.0.1
Published
React feature flag provider, hooks, variants, payloads, and exposure tracking for Vortos applications
Maintainers
Readme
@vortos/flags
React feature flag provider, hooks, variants, payloads, and exposure tracking for Vortos applications.
The backend evaluates rollout rules. This package keeps evaluated frontend flag state local, observable, refreshable, and easy to consume from React components.
Backend evaluates.
Frontend remembers.
Components ask locally.
Backend can change rollout without redeploying frontend.Install
npm install @vortos/flagsReact is a peer dependency:
react >= 18Basic Setup
Wrap your app once near the router:
import { FeatureFlagProvider } from '@vortos/flags';
export function App() {
return (
<FeatureFlagProvider endpoint="/api/flags">
<Router />
</FeatureFlagProvider>
);
}The endpoint can be any route. /api/flags is only the default Vortos convention.
<FeatureFlagProvider endpoint="/internal/frontend/flags">
<App />
</FeatureFlagProvider>Response Contract
Minimum response:
{
"flags": ["new-dashboard", "analytics-tab"]
}With variants:
{
"flags": ["new-checkout"],
"variants": {
"checkout-layout": "variant-b"
}
}With remote configuration payloads:
{
"flags": ["new-dashboard"],
"variants": {
"checkout-layout": "variant-b"
},
"payloads": {
"new-dashboard": {
"maxWidgets": 8,
"layout": "compact"
}
},
"version": "flags_2026_05_04_001"
}Use booleans for rollout, variants for experiments, and payloads for small public configuration. Do not put secrets in payloads because they are delivered to the browser.
Simple Hooks
import { FeatureFlag, useFlag, useVariant } from '@vortos/flags';
const enabled = useFlag('new-dashboard');
const variant = useVariant('checkout-layout', 'control');Render with a component:
<FeatureFlag name="new-checkout" fallback={<OldCheckout />}>
<NewCheckout />
</FeatureFlag>Stateful Hooks
Use useFlagState() when a screen needs loading, stale, refresh, or error state:
import { useFlagState } from '@vortos/flags';
function DashboardEntry() {
const { enabled, loading, error, stale, refetch } = useFlagState('new-dashboard');
if (loading) return <DashboardSkeleton />;
if (error) return <RetryPanel error={error} onRetry={refetch} />;
return enabled ? <NewDashboard stale={stale} /> : <OldDashboard />;
}Use useFlagContext() for global diagnostics:
import { useFlagContext } from '@vortos/flags';
function FlagDebugPanel() {
const { flags, variants, payloads, version, refreshing, refetch } = useFlagContext();
return (
<button disabled={refreshing} onClick={() => refetch()}>
Refresh flags
</button>
);
}Payloads
import { useFlagPayload } from '@vortos/flags';
type DashboardPayload = {
maxWidgets: number;
layout: 'compact' | 'comfortable';
};
const config = useFlagPayload<DashboardPayload>('new-dashboard', {
maxWidgets: 4,
layout: 'comfortable',
});
return <Dashboard maxWidgets={config.maxWidgets} layout={config.layout} />;Variants
const variant = useVariant('checkout-layout', 'control');
if (variant === 'variant-b') return <CheckoutB />;
return <CheckoutA />;Validate allowed variants:
const variant = useVariant('checkout-layout', {
default: 'control',
allowed: ['control', 'variant-a', 'variant-b'],
});For state plus exposure tracking:
const { variant, loading, error, trackExposure } = useVariantState(
'checkout-layout',
{
default: 'control',
allowed: ['control', 'variant-a', 'variant-b'],
trackExposure: true,
}
);Targeting Context
Pass targeting data to the backend evaluator:
<FeatureFlagProvider
endpoint="/api/flags"
context={{
userId,
tenantId,
role,
country,
plan: 'enterprise',
attributes: {
federationId,
betaGroup: 'coaches',
},
}}
>
<App />
</FeatureFlagProvider>The provider sends context in X-Vortos-Flag-Context by default. You can change the header:
<FeatureFlagProvider
endpoint="/api/flags"
context={flagContext}
contextHeaderName="X-App-Flag-Context"
>
<App />
</FeatureFlagProvider>Exposure Tracking
Use a callback:
<FeatureFlagProvider
endpoint="/api/flags"
onExposure={(event) => {
analytics.track('flag.exposure', event);
}}
>
<App />
</FeatureFlagProvider>Or post exposure events to an endpoint:
<FeatureFlagProvider
endpoint="/api/flags"
exposureEndpoint="/api/flags/exposures"
>
<App />
</FeatureFlagProvider>Exposure events are deduplicated per flag and variant during a provider lifecycle.
Auth Headers
Headers are part of the provider's refetch identity. If a token changes, flags refetch.
<FeatureFlagProvider
endpoint="/api/flags"
headers={{ Authorization: `Bearer ${token}` }}
>
<Router />
</FeatureFlagProvider>Refreshing, Stale State, And Cache
<FeatureFlagProvider
endpoint="/api/flags"
headers={{ Authorization: `Bearer ${token}` }}
context={{ userId, tenantId, role, plan }}
staleTime={30_000}
refreshInterval={60_000}
refetchOnWindowFocus
retries={2}
retryDelayMs={500}
persist
cacheKey={`flags:${userId}:${tenantId}`}
>
<Router />
</FeatureFlagProvider>Use tenant-aware cache keys:
flags:${userId}:${tenantId}This prevents one user's cached flags from appearing after another user logs in on the same browser.
SSR Initial Data
<FeatureFlagProvider
initialFlags={serverFlags}
initialVariants={serverVariants}
initialPayloads={serverPayloads}
initialVersion={serverFlagVersion}
>
<App />
</FeatureFlagProvider>The provider still refetches on the client after mount.
Permissions Versus Flags
Feature flags answer: should this product feature be visible or enabled right now?
Permissions answer: is this user allowed to perform this action?
Use both when a feature requires rollout and authorization:
import { useFlag } from '@vortos/flags';
import { usePermission } from '@vortos/permissions';
const rolledOut = useFlag('analytics-tab');
const allowed = usePermission('analytics.view.any');
return rolledOut && allowed ? <AnalyticsNav /> : null;