Standards Basics
10 examples for team conventions beyond ESLint - naming, folders, and native touch rules that survive SDK churn and new hires. Seven basic and three intermediate.
Search across all documentation pages
10 examples for team conventions beyond ESLint - naming, folders, and native touch rules that survive SDK churn and new hires. Seven basic and three intermediate.
Start from an Expo SDK 57 project with TypeScript and Expo Router. Standards live in docs/CONTRIBUTING.md (or docs/standards.md) and are enforced in PR review - not only in chat.
npx create-expo-app@latest MyApp --template default@sdk-57
cd MyApp
npx expo lint{
"dependencies": {
"expo": "~57.0.4",
"react": "19.2.3",
"react-native": "0.86.0"
}
}Tooling: These examples target Expo SDK 57 (
expo~57.0.4), React Native 0.86.0, and React 19.2.3. ESLint setup is in Linting Basics; project rules are in Expo Project Rules Checklist.
Publish one table - debate once, reference forever.
| Artifact | Convention | Example | Avoid |
|---|---|---|---|
| React components | PascalCase | OrderSummaryCard.tsx | orderSummary.tsx |
| Hooks | use + camelCase | useOrders.ts | OrdersHook.ts |
| Route folders (Expo Router) | kebab-case | app/order-history/ | app/orderHistory/ |
| Feature folders | kebab-case | src/features/order-history/ | src/features/OrderHistory/ |
| Constants (module scope) | SCREAMING_SNAKE | MAX_RETRY_COUNT | maxRetry |
| Test files | .test.ts(x) suffix | useOrders.test.ts | useOrders.spec.js |
| Env-specific config | development, preview, production | EAS profile names | dev, prod2 |
// src/features/order-history/components/OrderSummaryCard.tsx
export function OrderSummaryCard({ orderId }: { orderId: string }) {
return null;
}hooks/ inside the feature or in src/shared/hooks/ when cross-featureorderId) even when URL segment is kebab-caseRelated: Prettier & Import Sorting - formatting is not naming | Coding Standards & Style Guides - enforcement cookbook
Align with Mobile Architecture Basics: thin app/, fat src/features/.
MyApp/
├── app/ # routes only - no business logic
│ ├── (tabs)/
│ │ ├── index.tsx
│ │ └── orders/
│ │ └── [id].tsx
│ └── _layout.tsx
├── src/
│ ├── shared/ # api, ui primitives, config helpers
│ ├── entities/ # User, Order - domain nouns
│ └── features/
│ ├── auth/
│ │ ├── index.ts # public API
│ │ ├── screens/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── model/
│ └── orders/
├── plugins/ # config plugins - native touch zone
├── app.config.ts
├── eas.json
└── docs/
├── CONTRIBUTING.md # standards live here
└── adr/app/ files should re-export feature screens - one line when possiblesrc/features/<name>/index.ts is the only import path other modules useplugins/ and app.config.ts changes trigger native review - see native touch rules belowsrc/utils/ junk drawer - promote to shared/ with a named purposeRelated: Expo Project Rules Checklist - Tier 2 folder rules
// ✅ Correct - consumer imports from feature barrel
import { OrdersScreen, type Order } from "@/features/orders";
// ❌ Wrong - couples to internal refactor
import { OrderRow } from "@/features/orders/components/OrderRow";// src/features/orders/index.ts
export { OrdersScreen } from "./screens/OrdersScreen";
export type { Order } from "./model/types";
// Do not export OrderRow unless another feature truly needs itno-restricted-imports can enforce @/features/*/* paths - see Custom ESLint Rules for RNsrc/shared/ui/ - do not export sibling internals// app/(tabs)/orders/index.tsx
export { OrdersScreen as default } from "@/features/orders";// app/orders/[id].tsx - param parsing stays in the route layer
import { useLocalSearchParams } from "expo-router";
import { OrderDetailScreen } from "@/features/orders";
export default function OrderDetailRoute() {
const { id } = useLocalSearchParams<{ id: string }>();
if (!id) return null;
return <OrderDetailScreen orderId={id} />;
}_layout.tsx) own providers and stack/tab config - not feature business rules| Path / action | Default owner | PR requirement |
|---|---|---|
ios/, android/ (CNG gitignored) | EAS Build / expo prebuild | No manual commits - regenerate |
ios/, android/ (checked in) | Mobile platform squad | Two reviewers + device build_id |
plugins/*.ts | Platform + feature lead | expo prebuild --clean diff attached |
app.config.ts plugins array | Same as plugins | npx expo config --type public output in PR |
eas.json credentials | Release manager | No drive-by profile edits |
metro.config.js, babel.config.js | Infra / senior mobile | Explain bundle impact |
# After plugin or SDK change - mandatory on CNG teams
npx expo prebuild --clean
git diff ios/ android/Info.plist guarantees EAS overwritedocs/adr/ - "temporary" without ADR becomes years of driftRelated: Native Module Rules - when JS is not enough
feat/SHIP-412-order-summary-sheet
fix/SHIP-419-crash-on-logout
chore/upgrade-expo-sdk-57
spike/expo-ui-tab-bar
release/2.6.0
hotfix/2.5.1feat, fix, chore, spikespike/* never merges to main without ADR outcome - see Spikes, PoCs & New Architecturechore/upgrade-* branches - not mixed with feature workRelated: Git Basics for Mobile Teams
EAS environment: development | preview | production
.env files: .env.local (gitignored), .env.example (committed)
Public runtime vars: EXPO_PUBLIC_* only - never secrets
Config: app.config.ts reads process.env at build time// app.config.ts - pattern
const IS_DEV = process.env.APP_ENV === "development";
export default {
expo: {
extra: {
apiUrl: process.env.EXPO_PUBLIC_API_URL,
},
},
};EXPO_PUBLIC_* inlines at bundle time - treat as visible to users; see Security Rules for Mobile.env.example documents every required key with placeholder values - onboarding copies to .env.localeas.json match development / preview / production - no staging2apps/
mobile/ # Expo app - primary consumer
packages/
ui/ # @myorg/ui - shared RN components
api-client/ # @myorg/api-client - fetch layer
tsconfig/ # @myorg/tsconfig - shared bases{
"name": "@myorg/ui",
"main": "src/index.ts",
"peerDependencies": {
"react": "19.2.3",
"react-native": "0.86.0"
}
}src/index.ts - same barrel rule as featureslint / typecheck scripts - CI Quality GatesWhen adding a config plugin or custom native module:
plugins/
withMySdk.ts # config plugin
src/modules/
my-sdk/ # optional local Expo module
expo-module.config.json
ios/
android/# PR checklist for native-touch PRs
npx expo prebuild --clean
npx expo-doctor
eas build --profile preview --platform allwith* function name - one plugin per fileexpo prebuildCommit docs/CONTRIBUTING.md with enforceable bullets - link from PR template.
# Mobile Standards (SDK 57)
## Naming
- Components: PascalCase files matching export
- Routes: kebab-case folders under app/
## Imports
- Features: import only from @/features/<name>
- No default exports in hooks/utils - named exports only
## Native touch
- No manual ios/ android/ edits under CNG
- plugins/ changes require prebuild --clean diff in PR
## Review
- UI PRs: iOS + Android screenshot or build_id
- Native-touch PRs: platform reviewer required
## Exceptions
- Record in docs/adr/NNNN-title.md - link in PRESLint catches syntax and selected anti-patterns. It does not know your feature boundaries, native ownership, or spike merge policy. Standards document architecture and process ESLint cannot express without unmaintainable custom rules.
External API shapes may use snake_case in adapters (mapOrderFromApi). TypeScript/React layer stays camelCase - transform at the shared/api boundary, not in components.
Staff or lead mobile engineer + written ADR. Emergency hotfix may merge with retroactive ADR within one sprint - not "we'll document later" indefinitely.
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 19, 2026