1

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 useEffect sincroniza os dados do usuário com o ConfigCat, habilitando targeting por atributos
  • O segundo useEffect busca todas as flags de uma vez — boolean e string separadamente — e armazena no estado. user faz 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 featureControlar qual variante exibir
Proteger uma rotaA/B testing com múltiplas variantes
Kill switch de emergênciaConteú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

Carregando publicação patrocinada...