Coding Standards & Style Guides
A cookbook for RN/Expo style guide enforcement - ESLint, Prettier, TypeScript, PR review, and CODEOWNERS wired so standards survive turnover without a style sheriff in every thread.
Search across all documentation pages
A cookbook for RN/Expo style guide enforcement - ESLint, Prettier, TypeScript, PR review, and CODEOWNERS wired so standards survive turnover without a style sheriff in every thread.
Quick-reference recipe card - copy-paste ready.
// package.json - the four scripts every gate calls
{
"scripts": {
"lint": "expo lint",
"lint:ci": "expo lint -- --max-warnings 0",
"typecheck": "tsc --noEmit",
"format:check": "prettier --check .",
"test": "jest"
}
}# Local pre-push (identical sequence to CI)
npm run format:check && npm run lint:ci && npm run typecheck && npm run testEnforcement stack
Prettier → formatting (quotes, commas, import order)
ESLint → RN/React patterns, hooks, restricted imports
TypeScript → types strict - no implicit any in src/
PR template → human gates ESLint cannot cover
CODEOWNERS → native-touch and release config pathsWhen to reach for this:
div habits into RN.any, inline styles on lists, and deep feature imports.apps/mobile and packages/ui.When to avoid:
npx expo lint and ship; formalize when a second contributor joins.End-to-end enforcement for an Expo SDK 57 app from scaffold through merged PR.
Step 1 - Scaffold lint and format
npx create-expo-app@latest StyleApp --template default@sdk-57
cd StyleApp
npx expo lint
npm install --save-dev prettier @ianvs/prettier-plugin-sort-imports eslint-config-prettier// eslint.config.js - extend expo flat config
import expo from "eslint-config-expo/flat";
import prettier from "eslint-config-prettier";
export default [
{ ignores: [".expo/", "dist/", "ios/", "android/"] },
...expo,
prettier,
{
rules: {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
},
},
];Step 2 - TypeScript strict baseline
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"paths": { "@/*": ["./src/*"] }
},
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
}npm run typecheck # must pass before every PR mergetsc and ESLint catch different bugs - run both in CIStep 3 - Husky pre-commit (optional accelerator)
npm install --save-dev husky
npx husky init# .husky/pre-commit
npm run format:check && npm run lint && npm run typecheck--no-verifyStep 4 - Custom RN rules for architecture
// eslint.config.js - add restricted imports (see full recipes in custom rules page)
{
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["@/features/*/*"],
message: "Import from @/features/<name> barrel only - see CONTRIBUTING.md",
},
],
},
],
},
}StyleSheet.create in list renderItem hot paths via custom rule or review checklistconsole.log in src/ - use structured logging adapterStep 5 - GitHub Actions quality workflow
# .github/workflows/quality.yml
name: Quality Gates
on:
pull_request:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run format:check
- run: npm run lint:ci
- run: npm run typecheck
- run: npm run test -- --ciquality check - red lint cannot mergeturbo run lint typecheck test fans out across packagesStep 6 - PR template human gates
## Style & standards
- [ ] Imports use feature barrels only (`@/features/<name>`)
- [ ] No new `any` without `// eslint-disable-next-line` + ticket comment
- [ ] Lists use FlashList/FlatList - no ScrollView for dynamic feeds
- [ ] Native-touch files: prebuild diff attached (if applicable)
## Evidence
- [ ] iOS tested - build_id or screenshot
- [ ] Android tested - build_id or screenshotStep 7 - CODEOWNERS for config and native touch
# .github/CODEOWNERS
app.config.ts @mobile-platform
eas.json @mobile-release
plugins/ @mobile-platform
ios/ @mobile-platform
android/ @mobile-platform
.github/workflows/ @mobile-infraPublish these bullets in docs/STYLE.md - link from PR template.
| Rule | Do | Don't |
|---|---|---|
| Props | type ButtonProps = { label: string } | any props |
| Components | Named export or default export per file - pick one per folder | Mixed patterns |
| Nullability | order?.id with strict null checks | as casts to silence errors |
| Enums | as const objects | TypeScript enum (tree-shaking) |
// ✅ Pressable + accessibility
<Pressable
accessibilityRole="button"
accessibilityLabel="Submit order"
onPress={onSubmit}
>
<Text>Submit</Text>
</Pressable>
// ❌ TouchableOpacity without a11y props on interactive controlsPressable over legacy touchables for new codeStyleSheet.create at module scope - not inline objects in list rowsimport { useState } from "react";
import { View, Text } from "react-native";
import { useRouter } from "expo-router";
import { Button } from "@/shared/ui";
import { useOrders } from "@/features/orders";
import { styles } from "./styles";complexity rule| Severity | Example | Action |
|---|---|---|
| Block | Secret in EXPO_PUBLIC_*, deep feature import, missing expo-doctor on SDK PR | Request changes |
| Block | ScrollView + .map() for unbounded list | Request changes |
| Discuss | exhaustive-deps warning with documented exception | Comment + ticket |
| Nit | Variable rename outside PR scope | Approve or optional follow-up |
// turbo.json
{
"pipeline": {
"lint": { "dependsOn": ["^lint"] },
"typecheck": { "dependsOn": ["^typecheck"] },
"test": { "dependsOn": ["^test"] }
}
}# Root package.json
turbo run format:check lint typecheck testpackages/eslint-config exports flat config consumed by all apps.prettierignore skips ios/, android/, .expo/Pick one. This cookbook uses Prettier + @ianvs/prettier-plugin-sort-imports. Do not also enable eslint-plugin-import order rules - they fight on every save.
Fail on main and release/* with --max-warnings 0. Feature branches may tolerate warnings during migration sprints - but merge gate is zero warnings.
expo-rules are product/architecture non-negotiables (secrets, OTA, navigation). Style guides are code hygiene. Both appear in PR template; expo-rules violations are always blockers.
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