Reglas de Linting de React Hooks
Una lista de verificación para eslint-plugin-react-hooks en aplicaciones React Native de Expo - detectando llamadas ilegales de hooks, cierres obsoletos en código asincrónico móvil, y limpieza faltante antes de que se envíen a dispositivos en segundo plano.
- Ejecuta Tier 1 al estructurar o actualizar ESLint - confirma que
eslint-config-expo registra el plugin de hooks antes de agregar overrides.
- Aplica Tiers 2–3 por pantalla o hook personalizado durante la revisión de PR; los listeners móviles y los efectos de navegación fallan más a menudo de lo que sugieren los tutoriales de web
fetch.
- Trata cada
eslint-disable-next-line react-hooks/exhaustive-deps como una excepción documentada - anota por qué la dep es estable o por qué datos obsoletos son aceptables.
- Revisita Tier 4 después de actualizaciones de SDK - el batching de React 19 y la Nueva Arquitectura no eliminan la necesidad de arrays de dependencias correctos.
- Prueba dispositivos en pantallas que pasan linting pero aún muestran interfaz obsoleta después de backgrounding - linting no puede probar higiene de suscripción en tiempo de ejecución.
-
Confirma que eslint-plugin-react-hooks está activo: eslint-config-expo incluye el plugin en la config plana - ejecuta npx eslint --print-config app/index.tsx y verifica que las reglas react-hooks/ aparecen.
- Plugin faltante: Agrega
eslint-plugin-react-hooks y extiende su preset recomendado explícitamente.
- Registro duplicado: No instales el plugin dos veces mediante presets superpuestos - configs duplicados producen severidades conflictivas.
-
Severidad de react-hooks/rules-of-hooks: Mantén en error - el orden ilegal de hooks rompe React en producción y no puede degradarse a advertencia sin ocultar bloqueadores de envío.
- Gate de CI:
error falla expo lint y GitHub Actions de la misma manera que errores de TypeScript.
- Nunca global-off: Un simple
off para esta regla permite que hooks condicionales se fusionen silenciosamente.
-
Severidad de react-hooks/exhaustive-deps: Por defecto a warn en bases de código activas; promueve a error solo después de que el equipo tenga una política de exclusión escrita.
- Advertencia en CI: Empareja con
--max-warnings 0 cuando quieras cero advertencias sin luchar contra falsos positivos de referencia estable el primer día.
- Error para módulos nuevos: Scope
error a src/features/** una vez que las carpetas greenfield estén limpias.
-
Targeting de archivo de config plana: Asegúrate que app/, src/, y components/ estén dentro de globs files de ESLint - hooks en archivos de ruta se lintean igual que hooks compartidos.
- Monorepo: Extiende el mismo bloque de hooks a fuentes TypeScript compartidas
packages/ui - cierres obsoletos en un hook de sistema de diseño afectan cada pantalla.
- Ignora solo directorios generados: No ignores
app/ porque los nombres de archivo de Expo Router confunden al linter.
-
Integración del editor: Habilita ESLint en el IDE con soporte de config plana para que exhaustive-deps se muestre mientras escribes efectos, no solo en CI.
- Auto-fix al guardar: Auto-fix maneja el orden de importaciones, no arrays de dependencias - los desarrolladores deben editar deps manualmente.
- Pre-commit: Ejecuta
expo lint vía lint-staged en archivos .tsx cambiados.
-
Sin hooks dentro de renderItem: FlatList / SectionList renderItem es un callback, no un componente - extrae TodoRow y llama hooks en la parte superior de ese componente.
- Señal de lint:
rules-of-hooks marca llamadas de hook dentro de funciones anidadas.
- Patrón de fix:
renderItem={({ item }) => <TodoRow item={item} />}.
-
Sin hooks después de early returns: Las cláusulas de guardián por encima de useState / useEffect violan el orden de hooks cuando la guardiana se alterna entre renderizados.
- Común en RN:
if (!fontsLoaded) return null colocado antes de hooks en una pantalla.
- Fix: Mueve gates de carga debajo de todos los hooks o divide en un componente shell contenedor.
-
Prefija custom hooks con use: getFilters() puede llamar useState ilegalmente; useFilters() dispara rules-of-hooks en el cuerpo del hook personalizado.
- Convención de archivo:
use-app-state.ts exportando useAppState.
- Test helpers:
renderHook de Testing Library aún requiere un hook use* apropiado.
-
Sin hooks dentro de fábricas useMemo / useCallback: La fábrica se ejecuta durante renderizado; hooks pertenecen solo al nivel superior del componente.
- Olor: Crear una suscripción dentro de
useMemo(() => { useEffect(...) }).
- Fix:
useEffect plano con limpieza, o un hook personalizado dedicado.
-
Sin hooks en worklets de Reanimated o callbacks de no-React: Los worklets y callbacks de módulos nativos están fuera del ciclo de renderizado de React - usa shared values y runOnJS en su lugar.
- Brecha de lint: ESLint puede no analizar cuerpos de worklet - refuerza vía revisión de código y
// eslint-disable no es la respuesta.
- Ver: Custom Hooks for UI Logic para extracción de hook de plataforma.
-
Listeners de AppState: Efectos que se suscriben a AppState.addEventListener deben listar cada valor cerrado sobre el cual se usa dentro del handler, o leer valores frescos de refs.
- Patrón obsoleto: Handler lee
userId del renderizado cuando la app regresa del background después del logout.
- Fix: Incluye
userId en deps, o const userIdRef = useRef(userId) sincronizado en un efecto separado.
-
useFocusEffect de Expo Router / React Navigation: La identidad del callback importa - envuelve el trabajo en useCallback con deps correctas o eslint marca el wrapper de efecto.
- Patrón obsoleto: Refetch usa
route.params.id omitido de deps; registro incorrecto aparece después de deep link.
- Fix:
[route.params.id] en deps de useCallback; retorna limpieza para cancelar fetch en vuelo.
-
Suscripciones de NetInfo / conectividad: Callbacks de NetInfo.addEventListener que encolanan mutaciones deben ver auth actual y estado de cola.
- Patrón obsoleto: Replay de cola offline con token expirado capturado en mount.
- Fix: Dep en
accessToken o leer de un ref actualizado al refrescar.
-
Listeners de teclado y dimensiones: Efectos de Keyboard.addListener y Dimensions.addEventListener necesitan limpieza y deps para valores usados cuando se disparan.
- Patrón obsoleto:
keyboardVerticalOffset computado una vez; lint-clean pero incorrecto después de rotación.
- Fix: Dep en estado
layout o re-leer Dimensions.get dentro del handler.
-
Efectos asincrónico y AbortController: useEffect que llama a async function load() debe abortar en limpieza y listar deps que cambian la solicitud.
- Patrón obsoleto: Cambios rápidos de pestaña aplican una respuesta anterior - no capturado por deps solo sin abortar.
- Fix:
const ac = new AbortController(); … return () => ac.abort(); más array de dep completo.
-
Timers (setInterval, setTimeout): Efectos programando timers deben limpiarlos en limpieza e incluir deps que conducen el retardo.
- Patrón obsoleto: Polling interval aún golpea API después que la pantalla se desenfoca porque limpieza de
useFocusEffect fue olvidada.
- Fix: Limpia timer tanto en retorno de
useEffect como en limpieza de efecto de enfoque.
-
Refs estables vs deps faltantes: dispatch de useReducer, setState de useState, y refs son estables - omitirlos es fino; omitir props y estado derivado no es.
- Falso positivo: Lint quiere
dispatch - seguro de dejar; no deshabilites la regla por él.
- Bug real: Omitir
filter cuando efecto publica filter a analytics - agrega filter.
-
TanStack Query y contexto: Incluye data, isFetching, e inputs de query-key en deps cuando efectos reaccionan a ellos; queryClient de useQueryClient() es estable.
- Patrón obsoleto:
useEffect en data pero falta dataUpdatedAt cuando solo la frescura importa.
- Fix: Dep en los campos específicos que el efecto lee, o deriva un primitivo memoizado primero.
-
Política de disable de una línea: eslint-disable-next-line react-hooks/exhaustive-deps requiere un comentario inline explicando estabilidad o intención de obsolescencia.
- Rechaza: Disables en blanco para silenciar lint durante una prisa.
- Acepta:
// deps intencionalmente vacío - ping de analytics de mount-only.
-
Nunca deshabilites rules-of-hooks: Si la regla se dispara, refactoriza - hooks condicionales nunca son un falso positivo de lint.
-
Custom hooks exportan una historia de dep coherente: Hooks que envuelven AppState / NetInfo deben documentar qué valores los que llaman deben pasar para que efectos del que llama permanezcan lint-clean.
- Patrón:
useOnAppForeground(onForeground, deps) internamente refleja deps de useEffect.
- Anti-patrón: Estado de módulo global oculto dentro de un hook - los que llaman no pueden satisfacer exhaustive-deps honestamente.
-
Actualizaciones funcionales para eventos rápidos: setCount(c => c + 1) remueve count de handler deps - prefiere sobre deshabilitar lint en press handlers.
-
Lint de dependencia de useCallback / useMemo: Hooks anidados lintean sus propias fábricas - si useCallback omite un prop cerrado sobre el cual, el bug está en deps de callback, no exhaustive-deps en el efecto solo.
- Olor:
useEffect(() => { doWork(cb) }, [cb]) donde cb recrea cada renderizado de todas formas.
- Fix: Estabiliza
cb con deps correctas de useCallback o inline el trabajo en el efecto.
-
Aplicación de CI: Agrega expo lint (o eslint .) a verificaciones de PR con --max-warnings 0 cuando la política madura.
- Gradual: Comienza con
rules-of-hooks como error solo; voltea exhaustive-deps a error por paquete.
- Ver: CI Quality Gates para wiring de script.
- Tier 1 (1–5): Plugin y severidad - hazlo una vez por repo o actualización de ESLint.
- Tier 2 (6–10): Colocación de hook - arregla antes de revisar deps; hooks ilegales no pueden parchearse con ajustes de dep.
- Tier 3 (11–18): Async móvil y suscripciones - ROI más alto para interfaz obsoleta de producción en background/resume.
- Tier 4 (19–24): Disciplina de exclusión y CI - bloquea después que la base de código está mayormente verde.
¿Incluye eslint-config-expo las reglas de react-hooks?
Sí - el preset de config plana de Expo registra eslint-plugin-react-hooks. Confirma con npx eslint --print-config en un archivo de pantalla antes de agregar una instalación de plugin duplicada.
¿Debería exhaustive-deps ser error o advertencia?
Comienza en advertencia para que falsos positivos de referencia estable no bloqueen velocidad. Mueve a error (o --max-warnings 0) una vez que el equipo documenta exclusiones aceptables de una línea. Mantén rules-of-hooks en error siempre.
¿Por qué pasa linting pero mi pantalla muestra datos obsoletos después de resume?
exhaustive-deps es análisis estático - no puede verificar que tu handler de AppState lee estado de auth fresco. Audita listeners de Tier 3 manualmente y prueba background - foreground en un dispositivo físico.
¿Puedo llamar useFocusEffect sin useCallback?
Puedes, pero el efecto se re-ejecuta cada renderizado si la identidad del callback cambia. Envuelve el cuerpo en useCallback con los mismos deps que pondrías en un useEffect, o acepta refetches redundantes.
¿Es correcto omitir dispatch de deps?
Sí - useReducer dispatch es estable para la vida del componente. Lo mismo aplica a setters de useState. No deshabilites la regla únicamente para silenciar dispatch; agrega valores realmente faltantes en su lugar.
¿Cómo arreglo hooks dentro de FlatList renderItem?
Extrae un componente de fila: function Row({ item }) { const theme = useTheme(); … }. Pasa renderItem={({ item }) => <Row item={item} />}. Hooks entonces se ejecutan en un componente real.
¿Debería queryClient estar en mis deps de efecto?
Usualmente no - useQueryClient() retorna un cliente estable. Incluye resultados de query y variables que cambian comportamiento de fetch (id, filter), no el singleton del cliente.
¿Cuándo es eslint-disable-next-line aceptable?
Cuando puedes declarar en una línea por qué la dependencia es estable (mount-only, respaldada por ref, o deps vacío intencional). Nunca deshabilites generalizado para un archivo completo o directorio.
¿Cambian React 19 y Nueva Arquitectura estas reglas?
Batching y renderizado concurrente hacen cierres obsoletos más visibles, no menos. Reglas de hook y semántica de exhaustive-deps son sin cambios - aún refuérzalas en SDK 57.
¿Cómo hago lint de hooks en un paquete compartido de monorepo?
Aplica el mismo bloque de reglas react-hooks a fuentes TypeScript de packages/**. Un hook obsoleto en packages/ui se envía a cada app en el workspace.
¿Qué hay sobre useEffectEvent o APIs experimentales de React?
Si tu SDK envía una API estable oficial, sigue su orientación de lint. Hasta entonces, prefiere refs para handlers estables dentro de efectos en lugar de apagar exhaustive-deps globalmente.
¿Elimina useMemo la necesidad de exhaustive-deps?
No - useMemo tiene su propio array de dependencias. Efectos que leen valores memoizados aún necesitan esos valores (o sus deps) listados en el efecto.
¿Cómo pruebo limpieza de hook?
Usa renderHook con @testing-library/react-native, desmonta el hook, y afirma que listeners se removieron (mock AppState.addEventListener). Strict Mode double-mount en dev ayuda a superficie limpieza faltante durante ejecuciones manuales.
¿Puedo usar un override eslint global para pantallas?
Evita disables de carpeta ancha para app/. Las pantallas de ruta son donde useFocusEffect y efectos impulsados por params se concentran - necesitan lint más, no menos.
¿Dónde encajan los custom hooks de plataforma?
Envuelve AppState, NetInfo, y Keyboard en hooks use* con parámetros explícitos para valores cerrados sobre los cuales. Ver Custom Hooks for UI Logic.
Versiones de stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0, y Expo SDK 57 (expo ~57.0.4).