O problema que Feature Flags resolve (e por que usamos ConfigCat) — Parte 2: Implementação passo a passo
Na primeira parte, expliquei o problema que feature flags resolvem e a arquitetura que adotamos. Aqui vou direto à implementação.
A stack: Next.js 14, TypeScript strict, ConfigCat como engine e React Context API como camada de abstração.
Estrutura de pastas
src/
├── utils/consts/
│ └── featureFlags.ts # Constantes das flags + SDK keys
├── providers/feature-flags/
│ └── featureFlagsContext.tsx # Context + Provider + sincronização
└── pages/
└── _app.tsx # Provider na árvore de componentes
1. Instalação
npm install configcat-react
2. Constantes tipadas
// src/utils/consts/featureFlags.ts
export const featureFlags = {
SHOW_DASHBOARD: 'showDashboard',
ENABLE_NEW_CHECKOUT: 'enableNewCheckout'
} as const
export const stringFeatureFlags = {
ACTIVE_PROMOTION: 'activePromotion'
} as const
export type FeatureFlagKey = (typeof featureFlags)[keyof typeof featureFlags]
export type StringFeatureFlagKey = (typeof stringFeatureFlags)[keyof typeof stringFeatureFlags]
export const configCatSdkKeys = {
TEST: '<your-configcat-sdk-key-test>',
PRODUCTION: '<your-configcat-sdk-key-production>'
} as const
Por que tipar? O TypeScript garante que você não vai digitar o nome errado de uma flag. Se tentar usar uma string que não existe no objeto, o compilador avisa.
Por que dois objetos separados? Flags boolean e string têm contratos de retorno diferentes — { isEnabled, isLoading } vs { value, isLoading }. Separar os objetos permite tipar cada método de forma precisa, sem abrir mão de type safety.
Por que as SDK keys estão no código e não em .env? As SDK keys do ConfigCat são públicas por design — elas só permitem leitura das flags, não alteração. Colocá-las no código junto com uma função de detecção de ambiente elimina a necessidade de gerenciar arquivos .env por ambiente.
Exporte no barrel do projeto:
// src/utils/consts/index.ts
import { featureFlags, stringFeatureFlags, configCatSdkKeys } from './featureFlags'
const Consts = {
// ... outras constantes do projeto
featureFlags,
stringFeatureFlags,
configCatSdkKeys
}
export default Consts
// src/utils/index.ts
export type { FeatureFlagKey, StringFeatureFlagKey } from './consts/featureFlags'
3. Detecção de ambiente
Em vez de depender de variáveis de ambiente, uma função que detecta o ambiente em runtime:
// src/lib/getIsProduction.ts
const hostsProduction = [
'app.meudominio.com.br',
'app-stg.meudominio.com.br'
]
const getIsProduction = () =>
process.env.BUILD_ENV === 'production' ||
(typeof window !== 'undefined' &&
hostsProduction.includes(window.location.host))
export default getIsProduction
4. O Provider
Três responsabilidades: inicializar o ConfigCat com a SDK key correta, sincronizar os dados do usuário logado para targeting, e buscar todas as flags e expô-las via Context próprio.
// src/providers/feature-flags/featureFlagsContext.tsx
'use client'
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState
} from 'react'
import {
ConfigCatProvider as ConfigCatReactProvider,
useConfigCatClient,
User
} from 'configcat-react'
import { getIsProduction } from 'lib'
import { Consts, FeatureFlagKey, StringFeatureFlagKey } from 'utils'
import { UserContext } from 'providers/user/userContext'
type FlagState = {
value: boolean | string
loading: boolean
}
type FeatureFlagsContextProps = {
flags: Record<string, FlagState>
getFlag: (key: FeatureFlagKey) => { isEnabled: boolean; isLoading: boolean }
getStringFlag: (key: StringFeatureFlagKey) => { value: string; isLoading: boolean }
}
const defaultFlagState = { isEnabled: false, isLoading: true }
const defaultStringFlagState = { value: '', isLoading: true }
export const FeatureFlagsContext = createContext<FeatureFlagsContextProps>({
flags: {},
getFlag: () => defaultFlagState,
getStringFlag: () => defaultStringFlagState
})
O defaultFlagState retorna isEnabled: false e isLoading: true. Enquanto as flags não carregaram, nenhuma feature controlada por flag aparece — comportamento seguro por padrão.
ConfigCatSync — sincronização e reatividade
const ConfigCatSync = ({ children }: { children: React.ReactNode }) => {
const { user } = useContext(UserContext)
const client = useConfigCatClient()
const [flags, setFlags] = useState<Record<string, FlagState>>({})
// Sincroniza dados do usuário logado com o ConfigCat
useEffect(() => {
if (!client) return
if (user) {
const configCatUser = new User(user.email, user.email, undefined, {
role: user.role || '',
plan: user.plan || '',
is_admin: user.isAdmin ? '1' : '0'
})
client.setDefaultUser(configCatUser)
} else {
client.clearDefaultUser()
}
}, [client, user])
// Busca todas as flags e escuta mudanças
useEffect(() => {
if (!client) return
const fetchAllFlags = async () => {
const booleanKeys = Object.values(Consts.featureFlags)
const stringKeys = Object.values(Consts.stringFeatureFlags)
const booleanEntries = await Promise.all(
booleanKeys.map(async (key) => {
const value = await client.getValueAsync(key, false)
return [key, { value: Boolean(value), loading: false }] as const
})
)
const stringEntries = await Promise.all(
stringKeys.map(async (key) => {
const value = await client.getValueAsync(key, '')
return [key, { value: String(value), loading: false }] as const
})
)
setFlags(Object.fromEntries([...booleanEntries, ...stringEntries]))
}
fetchAllFlags()
const handler = () => {
fetchAllFlags()
}
client.on('configChanged', handler)
return () => {
client.off('configChanged', handler)
}
}, [client, user])
const getFlag = useCallback(
(key: FeatureFlagKey) => {
const flag = flags[key]
if (!flag) return defaultFlagState
return { isEnabled: Boolean(flag.value), isLoading: flag.loading }
},
[flags]
)
const getStringFlag = useCallback(
(key: StringFeatureFlagKey) => {
const flag = flags[key]
if (!flag) return defaultStringFlagState
return { value: String(flag.value), isLoading: flag.loading }
},
[flags]
)
const contextValue = useMemo(
() => ({ flags, getFlag, getStringFlag }),
[flags, getFlag, getStringFlag]
)
return (
<FeatureFlagsContext.Provider value={contextValue}>
{children}
</FeatureFlagsContext.Provider>
)
}
O que está acontecendo:
- O primeiro
useEffectsincroniza os dados do usuário com o ConfigCat, habilitando targeting por atributos - O segundo
useEffectbusca todas as flags de uma vez — boolean e string separadamente — e armazena no estado.userfaz parte das dependências: quando o usuário troca (login/logout), as flags são rebuscadas com o novo contexto - O
client.on('configChanged', handler)escuta mudanças: quando o ConfigCat detecta uma atualização via polling, o handler rebusca tudo automaticamente, sem refresh da página
O Provider principal
type FeatureFlagsProviderProps = {
children: React.ReactNode
}
const FeatureFlagsProvider = ({ children }: FeatureFlagsProviderProps) => {
const sdkKey = getIsProduction()
? Consts.configCatSdkKeys.PRODUCTION
: Consts.configCatSdkKeys.TEST
return (
<ConfigCatReactProvider
sdkKey={sdkKey}
options={{ pollIntervalSeconds: 3600 }}
>
<ConfigCatSync>{children}</ConfigCatSync>
</ConfigCatReactProvider>
)
}
export default FeatureFlagsProvider
Sobre o pollIntervalSeconds: o padrão do ConfigCat é 60 segundos. Usamos 3600 (1 hora) porque flags geralmente não precisam propagar imediatamente — e intervalos curtos consomem quota do plano desnecessariamente. Se o cenário exigir propagação mais rápida (kill switch urgente), reduzir para 60 ou 120 é razoável.
5. Adicionar na árvore de componentes
// src/pages/_app.tsx
import FeatureFlagsProvider from 'providers/feature-flags/featureFlagsContext'
function App({ Component, pageProps }) {
return (
<UserProvider>
<FeatureFlagsProvider>
<Component {...pageProps} />
</FeatureFlagsProvider>
</UserProvider>
)
}
O FeatureFlagsProvider precisa estar dentro do UserProvider, porque o ConfigCatSync consome o UserContext para sincronizar os dados do usuário.
Configurando no dashboard do ConfigCat
Flag boolean: crie do tipo On/Off Toggle com o nome exato da constante (ex: showDashboard). Valor padrão OFF.
Flag string: crie do tipo Text com o nome da constante em stringFeatureFlags (ex: activePromotion). Valor padrão string vazia.
Ambientes: o ConfigCat separa ambientes por SDK key. Cada ambiente tem sua própria key — você pode ter a flag ON em teste e OFF em produção ao mesmo tempo.
Consumindo nos componentes
Controlar visibilidade de um item no menu
import { useContext, useMemo } from 'react'
import { Consts } from 'utils'
import { FeatureFlagsContext } from 'providers/feature-flags/featureFlagsContext'
const Menu = ({ loggedUser }) => {
const { getFlag } = useContext(FeatureFlagsContext)
const { isEnabled: showDashboard } = getFlag(
Consts.featureFlags.SHOW_DASHBOARD
)
const menuItems = useMemo(() => {
const base = [
{ label: 'Parcelas', url: '/app/parcelas' },
{ label: 'Recibos', url: '/app/recibos' }
]
if (loggedUser.isAdmin && showDashboard) {
base.unshift({ label: 'Dashboard', url: '/app/dashboard' })
}
return base
}, [loggedUser, showDashboard])
return (
<nav>
<ul>
{menuItems.map((item) => (
<li key={item.url}>
<a href={item.url}>{item.label}</a>
</li>
))}
</ul>
</nav>
)
}
O componente importa FeatureFlagsContext — não sabe nada sobre ConfigCat.
Proteger uma página inteira com redirect
import { useContext, useEffect } from 'react'
import { useRouter } from 'next/router'
import { Consts } from 'utils'
import { FeatureFlagsContext } from 'providers/feature-flags/featureFlagsContext'
const DashboardPage = () => {
const { push } = useRouter()
const { getFlag } = useContext(FeatureFlagsContext)
const { isEnabled: showDashboard, isLoading } = getFlag(
Consts.featureFlags.SHOW_DASHBOARD
)
useEffect(() => {
if (!isLoading && !showDashboard) {
push('/app')
}
}, [isLoading, showDashboard, push])
if (isLoading) return null
return <div>Conteúdo do Dashboard</div>
}
Sem checar isLoading, o componente faria redirect imediatamente no primeiro render — porque isEnabled começa como false enquanto as flags carregam.
Exibição condicional
const { isEnabled: enableNewCheckout } = getFlag(
Consts.featureFlags.ENABLE_NEW_CHECKOUT
)
return enableNewCheckout ? <NewCheckout /> : <LegacyCheckout />
Flag string para conteúdo dinâmico
const { value: activePromotion, isLoading } = getStringFlag(
Consts.stringFeatureFlags.ACTIVE_PROMOTION
)
if (isLoading || !activePromotion) return null
return <Banner type={activePromotion} />
No dashboard você configura 'black-friday', 'cyber-monday' ou vazio (desligado). Zero deploy pra trocar a campanha.
User Targeting
Com os dados do usuário sincronizados no ConfigCatSync, você pode criar regras no dashboard:
Por email (testar com seu próprio usuário)
IF User.email IS ONE OF ["dev@empresa.com"]
THEN serve: ON
To everyone else: OFF
Por atributo (liberar pra admins)
IF User.is_admin EQUALS "1"
THEN serve: ON
To everyone else: OFF
Rollout por porcentagem
10% dos usuários: ON
90% dos usuários: OFF
Testes
Mock do ConfigCat para Jest
// __mocks__/configcat-react.js
module.exports = {
ConfigCatProvider: ({ children }) => children,
useConfigCatClient: () => ({
getValueAsync: (key, defaultValue) => Promise.resolve(defaultValue),
setDefaultUser: () => {},
clearDefaultUser: () => {},
on: () => {},
off: () => {}
}),
User: class User {
constructor() {}
}
}
// jest.config.js
module.exports = {
moduleNameMapper: {
'^configcat-react$': '<rootDir>/__mocks__/configcat-react.js'
}
}
Mock do Context nos testes de componentes
Caminho feliz (flag ligada):
jest.mock('providers/feature-flags/featureFlagsContext', () => ({
FeatureFlagsContext: {
_currentValue: {
flags: {},
getFlag: () => ({ isEnabled: true, isLoading: false }),
getStringFlag: () => ({ value: 'default', isLoading: false })
}
}
}))
Flag desligada:
jest.mock('providers/feature-flags/featureFlagsContext', () => ({
FeatureFlagsContext: {
_currentValue: {
flags: {},
getFlag: () => ({ isEnabled: false, isLoading: false }),
getStringFlag: () => ({ value: '', isLoading: false })
}
}
}))
Estado de carregamento:
jest.mock('providers/feature-flags/featureFlagsContext', () => ({
FeatureFlagsContext: {
_currentValue: {
flags: {},
getFlag: () => ({ isEnabled: false, isLoading: true }),
getStringFlag: () => ({ value: '', isLoading: true })
}
}
}))
Boas práticas
Nomenclatura consistente
Use camelCase nas flags (padrão do ConfigCat) e UPPER_SNAKE_CASE nas constantes:
// Bom — claro e descritivo
featureFlags = {
SHOW_DASHBOARD: 'showDashboard',
ENABLE_NEW_CHECKOUT: 'enableNewCheckout'
}
// Ruim — ambíguo
featureFlags = {
DASH: 'dash',
CHECKOUT: 'checkout'
}
Escolha o tipo certo de flag
| Use boolean quando... | Use string quando... |
|---|---|
| Ligar/desligar uma feature | Controlar qual variante exibir |
| Proteger uma rota | A/B testing com múltiplas variantes |
| Kill switch de emergência | Conteúdo dinâmico (banners, campanhas) |
Limpeza de flags antigas
Quando a feature estiver 100% liberada e estável, remova a flag do código, a constante e delete do dashboard. Flag esquecida é dívida técnica.
Use isLoading em páginas protegidas
Sempre cheque isLoading antes de tomar decisões como redirect:
// Bom — espera carregar antes de decidir
const { isEnabled, isLoading } = getFlag(Consts.featureFlags.SHOW_DASHBOARD)
useEffect(() => {
if (!isLoading && !isEnabled) {
push('/app')
}
}, [isLoading, isEnabled, push])
// Ruim — redireciona antes de saber o valor real
const { isEnabled } = getFlag(Consts.featureFlags.SHOW_DASHBOARD)
useEffect(() => {
if (!isEnabled) {
push('/app') // Vai redirecionar no primeiro render!
}
}, [isEnabled, push])
Conclusão
Com ConfigCat como engine e React Context API como camada de abstração:
- Deploy contínuo sem medo
- Testes em produção com segurança
- Kill switch instantâneo
- Targeting avançado por usuário
- Suporte a flags boolean e string
- Estado centralizado e reativo
- Arquitetura desacoplada — pronta pra trocar de ferramenta
- TypeScript garantindo type safety nas flags
O desacoplamento via Context API é o que torna a arquitetura sustentável a longo prazo. Amanhã, se precisarmos trocar de ferramenta, alteramos apenas o Provider — os componentes não mudam.
Recursos
Escrito por Guilherme Marucchi — LinkedIn