Regras de Lint para React Hooks
Um checklist de verificação para eslint-plugin-react-hooks em aplicativos Expo React Native - capturando chamadas ilegais de hooks, closures obsoletas em código mobile assíncrono e dependências ausentes antes que cheguem a dispositivos em segundo plano.
- Execute o Nível 1 ao configurar ou atualizar o ESLint - confirme se
eslint-config-expo registra o plugin de hooks antes de adicionar overrides.
- Aplique os Níveis 2–3 por tela ou hook customizado durante a revisão de PR; listeners mobile e efeitos de foco de navegação falham com mais frequência do que os tutoriais de
fetch para web sugerem.
- Trate cada
eslint-disable-next-line react-hooks/exhaustive-deps como uma exceção documentada - anote por que a dependência é estável ou por que dados obsoletos são aceitáveis.
- Revise o Nível 4 após atualizações de SDK - o batching do React 19 e a Nova Arquitetura não removem a necessidade de arrays de dependência corretos.
- Teste em dispositivos telas que passam no lint, mas ainda mostram UI obsoleta após serem colocadas em segundo plano - o lint não pode provar a higiene das assinaturas em tempo de execução.
-
Confirme que eslint-plugin-react-hooks está ativo: eslint-config-expo inclui o plugin na configuração flat - execute npx eslint --print-config app/index.tsx e verifique se as regras react-hooks/ aparecem.
- Plugin ausente: Adicione
eslint-plugin-react-hooks e estenda explicitamente seu preset recomendado.
- Registro duplicado: Não instale o plugin duas vezes através de presets sobrepostos - configurações duplicadas produzem severidades conflitantes.
-
Severidade de react-hooks/rules-of-hooks: Mantenha em error - a ordem ilegal de hooks quebra o React em produção e não pode ser rebaixada para warn sem ocultar bloqueadores de lançamento.
- Portão de CI:
error falha expo lint e GitHub Actions da mesma forma que erros de TypeScript.
- Nunca desativar globalmente: Um único
off para esta regra permite que hooks condicionais se misturem silenciosamente.
-
Severidade de react-hooks/exhaustive-deps: Padrão para warn em bases de código ativas; promova para error apenas após a equipe ter uma política de supressão escrita.
- Warn em CI: Combine com
--max-warnings 0 quando você quiser zero avisos sem lutar contra falsos positivos de refs estáveis no primeiro dia.
- Error para novos módulos: Escopo
error para src/features/** assim que as pastas de código novo estiverem limpas.
-
Direcionamento de arquivo de configuração flat: Certifique-se de que app/, src/ e components/ estejam dentro dos globs files do ESLint - hooks em arquivos de rota são lintados da mesma forma que hooks compartilhados.
- Monorepo: Estenda o mesmo bloco de hooks para
packages/ui compartilhado - closures obsoletas em um hook de sistema de design afetam todas as telas.
- Ignorar apenas diretórios gerados: Não ignore
app/ porque os nomes de arquivos do Expo Router confundem o linter.
-
Integração com o editor: Habilite o ESLint no IDE com suporte a flat-config para que exhaustive-deps apareça enquanto você digita efeitos, não apenas em CI.
- Correção ao salvar: O auto-fix lida com a ordem de importação, não com arrays de dependência - os desenvolvedores devem editar as dependências manualmente.
- Pre-commit: Execute
expo lint via lint-staged em arquivos .tsx alterados.
-
Sem hooks dentro de renderItem: renderItem do FlatList / SectionList é um callback, não um componente - extraia TodoRow e chame hooks no topo desse componente.
- Sinal do Lint:
rules-of-hooks sinaliza chamadas de hook dentro de funções aninhadas.
- Padrão de correção:
renderItem={({ item }) => <TodoRow item={item} />}.
-
Sem hooks após retornos antecipados: Cláusulas de guarda acima de useState / useEffect violam a ordem dos hooks quando a guarda alterna entre renders.
- Comum em RN:
if (!fontsLoaded) return null colocado antes dos hooks em uma tela.
- Correção: Mova os portões de carregamento para baixo de todos os hooks ou divida em um componente wrapper.
-
Prefira o prefixo use para hooks customizados: getFilters() pode chamar useState ilegalmente; useFilters() aciona rules-of-hooks no corpo do hook customizado.
- Convenção de arquivo:
use-app-state.ts exportando useAppState.
- Auxiliares de teste:
renderHook da Testing Library ainda requer um hook use* adequado.
-
Sem hooks dentro de fábricas useMemo / useCallback: A fábrica é executada durante o render; hooks pertencem apenas ao nível superior do componente.
- Sinal de alerta: Criar uma assinatura dentro de
useMemo(() => { useEffect(...) }).
- Correção:
useEffect simples com limpeza, ou um hook customizado dedicado.
-
Sem hooks em worklets Reanimated ou callbacks não-React: Worklets e callbacks de módulos nativos estão fora do ciclo de renderização do React - use valores compartilhados e runOnJS em vez disso.
- Lacuna no Lint: O ESLint pode não analisar corpos de worklets - aplique via revisão de código e
// eslint-disable não é a resposta.
- Veja: Custom Hooks for UI Logic para extração de hooks de plataforma.
-
Listeners AppState: Efeitos que assinam AppState.addEventListener devem listar todos os valores fechados usados dentro do manipulador, ou ler valores frescos de refs.
- Padrão obsoleto: O manipulador lê
userId do render quando o app retorna do background após logout.
- Correção: Inclua
userId nas dependências, ou const userIdRef = useRef(userId) sincronizado em um efeito separado.
-
useFocusEffect do Expo Router / React Navigation: A identidade do callback importa - envolva o trabalho em useCallback com as dependências corretas ou o eslint sinalizará o wrapper do efeito.
- Padrão obsoleto: Refetch usa
route.params.id omitido das dependências; um registro incorreto pisca após deep link.
- Correção:
[route.params.id] nas dependências de useCallback; retorne a limpeza para cancelar o fetch em andamento.
-
Listeners NetInfo / conectividade: Callbacks NetInfo.addEventListener que enfileiram mutações devem ver o estado de autenticação e fila atual.
- Padrão obsoleto: Reexecuta a fila offline com um token expirado capturado no momento da montagem.
- Correção: Dependa de
accessToken ou leia de uma ref atualizada no refresh.
-
Listeners de teclado e dimensões: Efeitos Keyboard.addListener e Dimensions.addEventListener precisam de limpeza e dependências para valores usados ao serem disparados.
- Padrão obsoleto:
keyboardVerticalOffset calculado uma vez; lint-clean mas incorreto após rotação.
- Correção: Dependa do estado
layout ou releia Dimensions.get dentro do manipulador.
-
Efeitos assíncronos e AbortController: useEffect que chama async function load() deve abortar na limpeza e listar dependências que mudam a requisição.
- Padrão obsoleto: Trocas rápidas de abas aplicam uma resposta mais antiga - não capturada pelas dependências sozinhas sem abortar.
- Correção:
const ac = new AbortController(); … return () => ac.abort(); mais o array de dependências completo.
-
Timers (setInterval, setTimeout): Efeitos que agendam timers devem limpá-los na limpeza e incluir dependências que controlam o atraso.
- Padrão obsoleto: O intervalo de polling ainda atinge a API após a tela ser desfocada porque a limpeza de
useFocusEffect foi esquecida.
- Correção: Limpe o timer tanto no retorno de
useEffect quanto na limpeza do focus-effect.
-
Refs estáveis vs. dependências ausentes: dispatch de useReducer, setState de useState, e refs são estáveis - omiti-los é aceitável; omitir props e estado derivado não é.
- Falso positivo: Lint quer
dispatch - seguro de deixar; não desative a regra por causa disso.
- Bug real: Omitir
filter quando o efeito posta filter para analytics - adicione filter.
-
TanStack Query e contexto: Inclua data, isFetching e entradas de chave de consulta nas dependências quando efeitos reagem a elas; queryClient de useQueryClient() é estável.
- Padrão obsoleto:
useEffect em data mas dataUpdatedAt ausente quando apenas a frescura importa.
- Correção: Dependa dos campos específicos que o efeito lê, ou derive primeiro um primitivo memorizado.
-
Política de desativação de uma linha: eslint-disable-next-line react-hooks/exhaustive-deps requer um comentário inline explicando a estabilidade ou obsolescência intencional.
- Rejeitar: Desativações em branco para silenciar o lint durante uma corrida.
- Aceitar:
// deps intencionalmente vazios - ping de analytics apenas na montagem.
-
Nunca desative rules-of-hooks: Se a regra disparar, refatore - hooks condicionais nunca são um falso positivo de lint.
-
Hooks customizados exportam uma história de dependência coerente: Hooks que envolvem AppState / NetInfo devem documentar quais valores os chamadores devem passar para que os efeitos dos chamadores permaneçam limpos pelo lint.
- Padrão:
useOnAppForeground(onForeground, deps) espelha internamente as dependências de useEffect.
- Anti-padrão: Estado oculto global dentro de um hook - os chamadores não podem satisfazer
exhaustive-deps honestamente.
-
Atualizações funcionais para eventos rápidos: setCount(c => c + 1) remove count das dependências do manipulador - prefira isso em vez de desativar o lint em manipuladores de clique.
-
Lint de dependência useCallback / useMemo: Hooks aninhados fazem o lint de suas próprias fábricas - se useCallback omitir uma prop fechada, o bug está nas dependências do callback, não em exhaustive-deps apenas no efeito.
- Sinal de alerta:
useEffect(() => { doWork(cb) }, [cb]) onde cb recria a cada renderização de qualquer forma.
- Correção: Estabilize
cb com as dependências corretas de useCallback ou inline o trabalho no efeito.
-
Aplicação em CI: Adicione expo lint (ou eslint .) às verificações de PR com --max-warnings 0 quando a política amadurecer.
- Gradual: Comece apenas com
rules-of-hooks como erro; mude exhaustive-deps para erro por pacote.
- Veja: CI Quality Gates para configuração de scripts.
- Tier 1 (1–5): Plugin e severidade - faça uma vez por repositório ou atualização do ESLint.
- Tier 2 (6–10): Posicionamento de hooks - corrija antes de revisar dependências; hooks ilegais não podem ser corrigidos com ajustes de dependência.
- Tier 3 (11–18): Assinaturas e async mobile - maior ROI para UI obsoleta em produção após background/resume.
- Tier 4 (19–24): Disciplina de supressão e CI - bloqueie após a base de código estar majoritariamente verde.
O eslint-config-expo inclui regras do react-hooks?
Sim - o preset de configuração flat do Expo registra eslint-plugin-react-hooks. Confirme com npx eslint --print-config em um arquivo de tela antes de adicionar uma instalação duplicada do plugin.
Exhaustive-deps deve ser erro ou aviso?
Comece com aviso para que falsos positivos de refs estáveis não bloqueiem a velocidade. Mude para erro (ou --max-warnings 0) assim que a equipe documentar supressões aceitáveis de uma linha. Mantenha rules-of-hooks em erro sempre.
Por que o lint passa mas minha tela mostra dados obsoletos após o resume?
exhaustive-deps é análise estática - ele não pode verificar se seu manipulador AppState lê o estado de autenticação atualizado. Audite os listeners do Tier 3 manualmente e teste background → foreground em um dispositivo físico.
Posso chamar useFocusEffect sem useCallback?
Você pode, mas o efeito é reexecutado a cada renderização se a identidade do callback mudar. Envolva o corpo em useCallback com as mesmas dependências que você colocaria em um useEffect, ou aceite refetches redundantes.
Omitir dispatch das dependências está correto?
Sim - o dispatch do useReducer é estável para o tempo de vida do componente. O mesmo se aplica aos setters do useState. Não desative a regra apenas para silenciar dispatch; adicione valores ausentes reais em vez disso.
Como corrijo hooks dentro do renderItem do FlatList?
Extraia um componente de linha: function Row({ item }) { const theme = useTheme(); … }. Passe renderItem={({ item }) => <Row item={item} />}. Os hooks então rodam em um componente real.
O queryClient deve estar nas minhas dependências de efeito?
Geralmente não - useQueryClient() retorna um cliente estável. Inclua os resultados da consulta e variáveis que mudam o comportamento de fetch (id, filter), não o singleton do cliente.
Quando eslint-disable-next-line é aceitável?
Quando você pode declarar em uma linha por que a dependência é estável (apenas na montagem, baseada em ref, ou dependências intencionalmente vazias). Nunca desative globalmente para um arquivo ou diretório inteiro.
O React 19 e a Nova Arquitetura mudam essas regras?
O batching e a renderização concorrente tornam as closures obsoletas mais visíveis, não menos. As regras de hooks e a semântica de exhaustive-deps permanecem inalteradas - ainda as aplique no SDK 57.
Como faço o lint de hooks em um pacote compartilhado de monorepo?
Aplique o mesmo bloco de regras react-hooks às fontes TypeScript de packages/**. Um hook obsoleto em packages/ui é enviado para todos os aplicativos no workspace.
E sobre useEffectEvent ou APIs experimentais do React?
Se o seu SDK fornecer uma API estável oficial, siga suas orientações de lint. Até lá, prefira refs para manipuladores estáveis dentro de efeitos em vez de desativar exhaustive-deps globalmente.
useMemo remove a necessidade de exhaustive-deps?
Não - useMemo tem seu próprio array de dependências. Efeitos que leem valores memorizados ainda precisam desses valores (ou suas dependências) listados no efeito.
Como testo a limpeza de hooks?
Use renderHook com @testing-library/react-native, desmonte o hook e afirme que os listeners foram removidos (simule AppState.addEventListener). O Strict Mode de montagem dupla em desenvolvimento ajuda a expor a limpeza ausente durante execuções manuais.
Posso usar um override global do eslint para telas?
Evite desativações em toda a pasta para app/. As telas de rota são onde useFocusEffect e efeitos baseados em parâmetros se concentram - elas precisam de lint mais, não menos.
Onde se encaixam os hooks de plataforma customizados?
Envolva AppState, NetInfo e Keyboard em hooks use* com parâmetros explícitos para valores fechados. Veja Custom Hooks for UI Logic.
Versões do Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (expo ~57.0.4).