Linting Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
Start from eslint-config-expo: npx expo lint scaffolds the flat config - do not cargo-cult a 2019 .eslintrc from a web repo.
Use ESLint flat config in new projects: eslint.config.js with eslint-config-expo/flat - legacy eslintrc is a migration tax, not a feature.
Script lint in package.json: expo lint or eslint . - every PR runs the same command locally and in CI.
Separate Prettier from ESLint: Format with Prettier; lint with ESLint - eslint-config-prettier prevents rule fights.
Sort imports with a Prettier plugin: @ianvs/prettier-plugin-sort-imports - consistent import order beats bike-shedding in review.
Extend expo/tsconfig.base before strict: Official base includes paths and JSX settings - strict flags layer on a working foundation.
Enable strict incrementally: noImplicitAny per folder beats a thousand-error wall - track progress in a living ADR.
Add noUncheckedIndexedAccess deliberately: Catches params[0] bugs in router code - enable after nullability cleanup in shared utils.
Run tsc --noEmit in CI: Types are not optional on mobile - ESLint does not replace the compiler.
Install eslint-plugin-react-hooks: rules-of-hooks and exhaustive-deps are non-negotiable - mobile code uses effects for AppState, focus, and NetInfo.
Audit exhaustive-deps for focus effects: useFocusEffect callbacks need stable deps - stale closures cause duplicate fetches on tab switch.
Avoid eslint-disable on hooks rules: If deps are "too noisy," fix the effect design - suppression hides real stale-data bugs.
Ban console.log in production paths: no-console with allow: ["warn", "error"] - use a logger wrapper for debug tiers.
Enforce a11y props with eslint-plugin-react-native-a11y: accessibilityLabel on icon-only Pressables - catch in CI, not in App Store review.
Restrict cross-feature imports: no-restricted-paths or Nx boundaries - features/billing must not import features/orders/internals.
Custom local rules for org invariants: Small createRule plugins for banned APIs (AsyncStorage in wrong layer) - document in CONTRIBUTING.
CI runs lint, typecheck, format:check, test: Four scripts, one validate meta-script - PRs do not merge on partial green.
Cache ESLint in GitHub Actions: actions/cache on .eslintcache - monorepos need cache keys per package.
Mirror gates in EAS Workflows: Same validate before eas build - native binaries should not ship from unlinted branches.
Run Knip on a schedule: Unused exports and dead files accumulate in RN apps with Expo Router entrypoints - quarterly Knip PRs stay small.
Pair Knip with depcheck: Knip finds unused code; depcheck finds unused package.json deps - native modules left installed inflate prebuild time.
Whitelist Expo Router entries in Knip: app/** routes are dynamic - configure entry patterns or false positives block adoption.
Zero-warning policy with a sunset list: New warnings fail CI; legacy warnings have ticket IDs - velocity without infinite debt.
Auto-fix on save in editor, full check in CI: Developers get fast feedback; CI remains authoritative - do not skip CI because "it looked fine locally."
Document suppressions with why comments: eslint-disable-next-line requires a one-line justification - @ts-expect-error preferred over @ts-ignore for the same reason.
format:check is the enforcement layer.src/ and features/.Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026