Fundamentos del Linting
10 ejemplos para empezar con Linting & Formatting - 7 básicos y 3 intermedios.
Busca en todas las páginas de la documentación
10 ejemplos para empezar con Linting & Formatting - 7 básicos y 3 intermedios.
Comienza desde un proyecto Expo SDK 57. La plantilla default@sdk-57 ya incluye TypeScript y los scripts recomendados - solo necesitas agregar ESLint una vez.
npx create-expo-app@latest MyApp --template default@sdk-57
cd MyApp
npx expo lintnpx expo lint instala eslint, eslint-config-expo y escribe eslint.config.js en la raíz del proyecto. Confirma el pin de SDK antes de personalizar rules:
{
"dependencies": {
"expo": "~57.0.4",
"react": "19.2.3",
"react-native": "0.86.0"
},
"devDependencies": {
"eslint": "^9.0.0",
"eslint-config-expo": "~57.0.0"
}
}Tooling: Estos ejemplos apuntan a Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 y React 19.2.3. Instala la extensión ESLint de VS Code para que las violaciones aparezcan mientras editas.
npx expo lintEl CLI de Expo es el camino soportado hacia una flat config que funciona - fija versiones compatibles de eslint y eslint-config-expo para tu SDK.
# Primera ejecución: instala devDependencies y crea eslint.config.js
npx expo lint
# Ejecuciones posteriores: lint del proyecto completo
npx expo lint
# Auto-fix de violaciones seguras
npx expo lint --fixnpx expo lint lee el script lint desde package.json y reenvía flags a ESLinteslint.config.js y en el lockfile juntos.eslintrc.* restante antes de adoptar flat config - ESLint 9 prefiere flat config pero vuelve a legacy cuando ambos existenRelated: ../project-setup/project-setup-basics.md - pin de SDK y checklist de verificación de primera ejecución
eslint-config-expo por DefectoLos proyectos SDK 53+ usan flat config de ESLint. El archivo scaffolded extiende el preset de Expo - React, hooks, TypeScript e import rules ya están cableados.
// eslint.config.js
const { defineConfig } = require("eslint/config");
const expoConfig = require("eslint-config-expo/flat");
module.exports = defineConfig([
expoConfig,
{
ignores: ["dist/*"],
},
]);eslint-config-expo/flat exporta un array defineConfig - extiéndelo o referencíalo como el primer bloque para que los defaults de Expo carguen antes de tus overrides__DEV__, fetch, APIs amigables con Hermes) para que los archivos de pantalla hagan lint sin comentarios /* global */@typescript-eslint aplican a archivos .ts y .tsx cuando TypeScript está presente en el proyectoexpoConfig para que hagas override en lugar de reemplazar el presetRelated: Prettier & Import Sorting - capa Prettier sin pelear contra ESLint formatting rules
npm run lintCableia linting en package.json para que teammates, pre-commit hooks y CI todos invoquen el mismo comando.
{
"scripts": {
"start": "expo start",
"lint": "expo lint",
"lint:fix": "expo lint --fix",
"typecheck": "tsc --noEmit"
}
}npm run lint
npm run lint:fix"lint": "expo lint" es lo que npx expo lint ejecuta bajo el capó - prefiere el npm script en docs y YAML de CI para que el punto de entrada permanezca obviolint:fix para limpieza local; evita auto-fix en CI a menos que tu pipeline sea idempotente y rápidatypecheck con lint - ESLint captura patrones que TypeScript ignora (hook deps, import order) y viceversaRelated: CI Quality Gates - falla builds en lint y typecheck juntos
Comienza con un pequeño bloque de rules después del preset de Expo. Estos dos ajustes cubren la mayoría de nuevos equipos de Expo sin convertir lint en un proyecto de reescritura.
// eslint.config.js
const { defineConfig } = require("eslint/config");
const expoConfig = require("eslint-config-expo/flat");
module.exports = defineConfig([
expoConfig,
{
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"react/react-in-jsx-scope": "off",
},
},
{
ignores: ["dist/*", ".expo/*"],
},
]);// app/index.tsx - `_event` es intencionalmente no utilizado; lint pasa
import { Pressable, Text } from "react-native";
export default function Home() {
return (
<Pressable onPress={(_event) => console.warn("pressed")}>
<Text>Tap me</Text>
</Pressable>
);
}argsIgnorePattern: "^_" es la escotilla estándar para event handlers que debes aceptar pero no leesreact/react-in-jsx-scope: off es correcto para React 17+ JSX transform - el preset de Babel de Expo ya lo habilita"warn" temporalmente cuando lanzas una rule en un gran codebase brownfieldRelated: TypeScript Strict Mode - alinea strictness del compilador con lint rules a lo largo del tiempo
Los caminos generados y vendidos nunca deben bloquear un merge. En flat config, pon ignores en un objeto dedicado con ignores como la única key.
// eslint.config.js
const { defineConfig, globalIgnores } = require("eslint/config");
const expoConfig = require("eslint-config-expo/flat");
module.exports = defineConfig([
globalIgnores([
"node_modules/**",
".expo/**",
"dist/**",
"web-build/**",
"ios/**",
"android/**",
]),
expoConfig,
{
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
},
]);globalIgnores (o un objeto { ignores: [...] } standalone) salta archivos completamente - ejecuciones más rápidas y menos falsos positivos.expo/ - estado local del dev server, no sourceios/ y android/ en managed workflow hasta que intencionalmente hagas commit de proyectos nativos (CNG / bare)ignores con rules en el mismo objeto crea un filtro de scope de archivo, no un skip global - mantén ignores aisladosRelated: Dead Code & Dependency Analysis - encuentra archivos no utilizados que lint nunca toca
--fixApunta a una carpeta de feature mientras iteras para que el feedback permanezca rápido en apps grandes.
# Lint solo el árbol del router de la app
npx expo lint app
# Lint de un paquete compartido en un monorepo
npx expo lint packages/ui/src
# Arregla problemas auto-fixables en una pantalla
npx expo lint app/(tabs)/index.tsx --fix// app/(tabs)/index.tsx - hook dep faltante marcada antes de abrir un PR
import { useEffect, useState } from "react";
import { Text, View } from "react-native";
export default function TabHome() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((c) => c + 1), 1000);
return () => clearInterval(id);
}, []); // eslint puede advertir: react-hooks/exhaustive-deps
return (
<View>
<Text>Count: {count}</Text>
</View>
);
}app/ coincide con cómo Expo Router teams organizan features - lint la carpeta que estás activamente cambiando--fix reescribe import order y problemas simples de style; no inventará arrays de hook dependency para tireact-hooks/exhaustive-deps frecuentemente exponen closures obsoletos en timers y subscriptions - lee el mensaje antes de silenciarlonpm run lint permanece como el merge gate incluso cuando haces lint localmente por caminoRelated: React Hooks Lint Rules - patrones exhaustive-deps en código async mobile
Las app screens corren en Hermes o el browser; metro.config.js y app.config.ts corren en Node. El preset de Expo ya relaja Node globals para filenames de config comunes - extiende la lista cuando agregas archivos de tooling personalizados.
// eslint.config.js
const { defineConfig } = require("eslint/config");
const expoConfig = require("eslint-config-expo/flat");
const globals = require("globals");
module.exports = defineConfig([
expoConfig,
{
files: ["metro.config.js", "app.config.ts", "babel.config.js"],
languageOptions: {
globals: globals.node,
},
},
{
ignores: ["dist/*"],
},
]);// metro.config.js - __dirname es válido bajo Node globals
const { getDefaultConfig } = require("expo/metro-config");
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
module.exports = config;__dirname, require y module como undefined en archivos de configeslint-config-expo ya especializa varios caminos de config - agrega bloques files explícitos solo para nombres personalizados (tailwind.config.js, scripts/**)/* eslint-env node */ como último recurso en un archivo legacy único - prefiere centralizado languageOptions en flat config.js o .ts en la raíz del repo para que el glob de files permanezca simpleRelated: ../project-setup/project-setup-basics.md - layout de
metro.config.jsyapp.config.ts
Después de que la baseline pase, agrega rules enfocadas en mobile: prohíbe raw text fuera de <Text>, desalienta console.log en código de envío y requiere testID en pressables que cubres en E2E.
// eslint.config.js
const { defineConfig } = require("eslint/config");
const expoConfig = require("eslint-config-expo/flat");
module.exports = defineConfig([
expoConfig,
{
rules: {
"no-console": ["warn", { allow: ["warn", "error"] }],
"react-native/no-raw-text": [
"error",
{ skip: ["Trans", "Animated.Text"] },
],
},
},
{
ignores: ["dist/*", ".expo/*"],
},
]);// Violación: string desnudo como hijo de View - envuelve con Text
import { View, Text, Pressable } from "react-native";
export function SettingsRow({ label }: { label: string }) {
return (
<Pressable accessibilityRole="button" testID="settings-row">
<View>
<Text>{label}</Text>
</View>
</Pressable>
);
}react-native/no-raw-text captura una clase de crash común en Android - strings deben vivir dentro de <Text>no-console como warn mantiene logs de debug visibles en review sin bloquear merges el primer díaeslint-plugin-react-native cuando habilitas RN-specific rules: npx expo install eslint-plugin-react-native --devRelated: Custom ESLint Rules for RN - prohíbe
console.log, props a11y e import boundaries
En un layout de Turborepo o npm-workspaces, apunta ESLint a las raíces de source de cada app y comparte una base config de packages/eslint-config.
{
"name": "mobile",
"private": true,
"scripts": {
"lint": "expo lint app src",
"lint:all": "turbo run lint"
}
}// apps/mobile/eslint.config.js
const { defineConfig } = require("eslint/config");
const expoConfig = require("eslint-config-expo/flat");
const baseRules = require("@myorg/eslint-config/expo");
module.exports = defineConfig([
expoConfig,
baseRules,
{
ignores: ["dist/*", ".expo/*", "ios/**", "android/**"],
},
]);expo lint cuando el app code vive fuera de app/ (ej. src/features/)packages/eslint-config para que mobile y web apps hereden las mismas reglas de unused-var e importlint:all desde la raíz del repo en CI para que un cambio de shared hook en packages/ui falle antes de publicarignores - native folders y build output difieren por targetRelated: ../project-setup/monorepo-with-turborepo.md - workspace layout y task graph | CI Quality Gates - matrix builds por app
Trata lint y TypeScript como gates complementarios antes de eas build o una store submission - capturan diferentes clases de defecto.
{
"scripts": {
"lint": "expo lint",
"typecheck": "tsc --noEmit",
"validate": "npm run typecheck && npm run lint"
}
}npm run validate// TypeScript pasa; ESLint marca el hook - validate captura ambos
import { useCallback, useState } from "react";
import { Button, View } from "react-native";
export function Counter() {
const [n, setN] = useState(0);
const increment = useCallback(() => setN(n + 1), []); // stale closure risk
return (
<View>
<Button title={`${n}`} onPress={increment} />
</View>
);
}tsc --noEmit valida types sin emitir JS - lo suficientemente rápido para cada PRexpo lint ejecuta el hooks plugin - closures obsoletos como useCallback(..., []) leyendo n son lint findings, no type errorsvalidate o check y llámalo desde CI, pre-push hooks y rituales locales de pre-releasevalidate - nunca saltes el gate porque el simulator lanzóRelated: TypeScript Strict Mode - aprieta
strictincrementalmente | Linting Best Practices - política de cero advertencias sin ralentizar velocity
Stack versions: Esta página fue escrita para React 19.2.3, React Native 0.86.0 y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026