export const FLAG_REGISTRY = {
newDashboard: {
default: false as boolean,
description: 'Renders the redesigned dashboard shell',
},
maxUploadMb: {
default: 25 as number,
description: 'Client-side upload size cap',
},
checkoutVariant: {
default: 'control' as 'control' | 'compact' | 'express',
description: 'Active checkout layout experiment arm',
},
betaSearch: {
default: false as boolean,
description: 'Enables the incremental search index',
},
} as const;
export type FlagRegistry = typeof FLAG_REGISTRY;
export type FlagKey = keyof FlagRegistry;
export type FlagValue<K extends FlagKey> = FlagRegistry[K]['default'];
export type FlagValues = {
[K in FlagKey]: FlagValue<K>;
};
export function defaults(): FlagValues {
const out = {} as FlagValues;
(Object.keys(FLAG_REGISTRY) as FlagKey[]).forEach((key) => {
(out as Record<string, unknown>)[key] = FLAG_REGISTRY[key].default;
});
return out;
}
import React, { createContext, useMemo } from 'react';
import { FLAG_REGISTRY, FlagKey, FlagValues, defaults } from './flags';
export const FlagContext = createContext<FlagValues>(defaults());
function evaluate(overrides: Record<string, unknown>): FlagValues {
const resolved = defaults();
(Object.keys(FLAG_REGISTRY) as FlagKey[]).forEach((key) => {
if (!(key in overrides)) return;
const incoming = overrides[key];
const expected = typeof FLAG_REGISTRY[key].default;
if (typeof incoming !== expected) {
console.warn(`Ignoring flag "${key}": expected ${expected}`);
return;
}
(resolved as Record<string, unknown>)[key] = incoming;
});
return resolved;
}
type Props = {
overrides?: Record<string, unknown>;
children: React.ReactNode;
};
export function FlagProvider({ overrides = {}, children }: Props) {
const value = useMemo(() => evaluate(overrides), [overrides]);
return <FlagContext.Provider value={value}>{children}</FlagContext.Provider>;
}
import { useContext } from 'react';
import { FlagContext } from './FlagProvider';
import { FlagKey, FlagValue, FlagValues } from './flags';
export function useFlags(): FlagValues {
return useContext(FlagContext);
}
export function useFlag<K extends FlagKey>(key: K): FlagValue<K> {
const flags = useContext(FlagContext);
return flags[key];
}
export function useVariant<K extends FlagKey>(
key: K,
arms: Record<string, () => void>,
): void {
const value = String(useFlag(key));
const run = arms[value];
if (run) run();
}
This snippet shows how to model feature flags as a single strongly-typed registry rather than a loose bag of string keys, so that flag names and their value shapes are checked at compile time. The core idea is to make the flag definitions the source of truth: everything else — evaluation, the React context, and the consuming hook — derives its types from that one object.
In flags.ts, the FLAG_REGISTRY object declares every flag with its default value and a small description. The exported FlagKey type is keyof typeof FLAG_REGISTRY, and FlagValue<K> extracts the concrete type of each flag's default. Because the registry mixes booleans, numbers, and a string-union (checkoutVariant), the derived types stay precise per flag instead of collapsing to a generic boolean. This is the payoff of the typed-registry pattern: adding, renaming, or retyping a flag ripples through the whole codebase as type errors, catching stale references before they ship.
FlagProvider.tsx builds the runtime layer. evaluate merges server-provided overrides on top of the registry defaults, and crucially validates each override against the default's typeof so a malformed payload can't inject the wrong type at runtime — the static types assume the data is well-formed, so this guard protects the boundary where untyped JSON enters. The resolved values are memoized and exposed through a React context, keeping evaluation off the render path.
useFlag.ts is the consumer surface. The generic signature useFlag<K extends FlagKey>(key: K): FlagValue<K> means the return type is inferred from the key argument: useFlag('checkoutVariant') is typed as the string union, while useFlag('newDashboard') is a boolean. The useFlags helper returns the whole resolved map for components that read several flags at once.
The trade-off is that all flags live in one client-visible registry, so this fits build-time and lightweight runtime toggles rather than secret server gating. A common pitfall is treating overrides as trusted; the evaluate guard exists precisely because remote config is the least trustworthy input. This approach shines when a team wants autocomplete, safe deletion of dead flags, and confidence that every flag read matches its declared shape.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
class AddSettingsToAccounts < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_column :accounts, :settings, :jsonb, null: false, default: {}
Postgres JSONB Partial Index for Feature Flags
import { Application } from "@hotwired/stimulus"
import FormSubmitController from "./controllers/form_submit_controller"
const application = Application.start()
application.debug = false
Disable submit button while Turbo form is submitting
Share this code
Here's the card — post it anywhere.