Expo and React Native Coding Best Practices
Use these conventions to keep an Expo application predictable, testable, accessible, and safe to change as the product grows. They complement the detailed architecture, components, hooks, styling, state-management, and testing guides.
Index
- Project boundaries
- TypeScript and validation
- Components and hooks
- State and data fetching
- Errors and observability
- Performance
- Accessibility
- Security
- Testing and delivery
- Pull request checklist
Project boundaries
Organize most product code around features. Keep Expo Router files focused on route composition, navigation parameters, and screen-level layout.
app/
├── (tabs)/
│ └── projects/
│ ├── index.tsx
│ └── [projectId].tsx
features/
└── projects/
├── api/
├── components/
├── hooks/
├── schemas/
├── types.ts
└── __tests__/
components/
└── ui/
lib/
└── infrastructure/
Rules
- Route files compose features; they do not implement database or API clients.
- Feature-specific code stays inside its feature until another feature genuinely shares it.
- Shared UI primitives contain no product-specific business rules.
- Infrastructure modules wrap third-party services behind a small application-owned interface.
- Avoid circular imports and generic
helpers.tsfiles that collect unrelated behavior. - Prefer the
@/alias over long relative import chains.
Read Application Architecture and Adding Features for the complete project structure.
TypeScript and validation
Use strict TypeScript throughout the app. Types protect code written inside the repository, but external values still require runtime validation.
Validate data from:
- Network responses and webhooks
- Deep-link and route parameters
- Environment variables
- AsyncStorage, SecureStore, and SQLite
- Push-notification payloads
- Third-party SDK callbacks
import { z } from 'zod';
const projectSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
updatedAt: z.string().datetime(),
});
export type Project = z.infer<typeof projectSchema>;
export function parseProject(input: unknown): Project {
return projectSchema.parse(input);
}
Rules
- Use
unknowninstead ofanyfor untrusted values. - Prefer discriminated unions for loading and domain states.
- Avoid non-null assertions unless an invariant is enforced immediately before use.
- Do not duplicate API and domain types when a deliberate mapping function is clearer.
- Keep secrets out of
EXPO_PUBLIC_*variables; those values are embedded in the client bundle.
See Types Reference for the project types.
Components and hooks
Components should have one clear responsibility. Extract a component when it owns meaningful behavior, improves accessibility, or is reused—not only to shorten a file.
Components
- Use semantic React Native primitives and accessible labels for interactive controls.
- Keep loading, empty, error, and success states close to the component that renders them.
- Prefer explicit variants over many unrelated boolean props.
- Keep list keys stable; never use the array index for reorderable data.
- Do not start network requests during render.
Hooks
- Hooks must start with
useand follow the Rules of Hooks. - Return a small, documented interface rather than leaking third-party SDK objects.
- Include every reactive dependency in effects and callbacks.
- Clean up timers, subscriptions, and listeners.
- Avoid an effect when the value can be derived during render.
Read Components and Hooks for project examples.
State and data fetching
Give each kind of state one owner:
| State | Recommended owner |
|---|---|
| Remote API data | TanStack Query |
| Form input and validation | Form component or form library |
| Temporary UI state | Nearest owning component |
| Cross-feature session or theme | Focused context/provider |
| Persisted preferences | Storage adapter plus a typed hook |
| Navigation state | Expo Router |
Rules
- Do not copy query data into context or component state without a specific editing workflow.
- Use query keys consistently and include every value that changes the request.
- Invalidate the narrowest affected queries after a mutation.
- Design optimistic updates with rollback behavior.
- Distinguish the initial load from background refreshes.
- Surface offline and pending-sync states to the user.
Read State Management for the established patterns.
Errors and observability
Errors should help the user recover and help the team diagnose the failure without exposing sensitive data.
try {
await updateProfile(input);
} catch (error) {
reportError(error, { operation: 'profile_update' });
showToast('We could not save your profile. Try again.');
}
Rules
- Never silently swallow a failure.
- Add useful operation context, but exclude tokens, passwords, payment data, and personal content.
- Define analytics event names centrally and keep their properties stable.
- Avoid logging entire request or response objects.
- Use error boundaries for recoverable rendering failures.
- Give background operations explicit retry and terminal-failure behavior.
Read Error Tracking and Analytics.
Performance
Profile before adding memoization. Optimize user-visible bottlenecks rather than applying memo, useMemo, or useCallback everywhere.
- Use
FlatListor another virtualized list for large collections. - Paginate remote results and avoid one request per rendered row.
- Serve images close to their rendered dimensions and specify their size.
- Keep animation work on transform and opacity where possible.
- Move expensive synchronous work away from interactions.
- Test startup, navigation, and list scrolling on a representative physical device.
- Monitor bundle and native dependency growth before adding packages.
Accessibility
Accessibility is part of the component contract.
- Give every interactive control an accessible name and role.
- Use touch targets of at least 44 × 44 points when practical.
- Do not communicate meaning using color alone.
- Support screen readers, larger text, reduced motion, and keyboard focus on web.
- Announce important asynchronous changes when the interface does not otherwise expose them.
- Test critical flows with VoiceOver or TalkBack before release.
Security
- Store sensitive tokens with SecureStore or the mechanism required by the authentication provider.
- Treat AsyncStorage as unencrypted storage.
- Enforce authorization on the backend; hiding a button is not access control.
- Validate deep links and never execute an action from an untrusted payload without confirmation.
- Redact secrets and personal data from logs, analytics, and crash reports.
- Keep dependencies current and review native packages before installation.
- Use platform network security defaults and HTTPS for production traffic.
Read Authentication and Database for implementation details.
Testing and delivery
Use the smallest test that gives confidence in the behavior:
- Unit tests for pure domain rules and transformations
- Hook and component tests for user-visible state transitions
- Integration tests for provider and persistence boundaries
- End-to-end tests for authentication, onboarding, purchases, and other critical flows
Every pull request should run formatting, linting, TypeScript, and relevant tests. Production releases should also verify environment configuration, migrations, analytics, error reporting, deep links, and store builds.
Read the complete Testing Guide.
Pull request checklist
- The change follows an existing feature boundary or documents a new one
- External data is validated at runtime
- Loading, empty, error, success, and offline states are handled where relevant
- Effects clean up subscriptions, listeners, and timers
- Interactive controls are accessible
- No secret or personal data is logged or tracked
- Critical behavior has an appropriate test
- Lint and TypeScript pass
- Documentation is updated when public behavior or setup changes
- The change was tested on the affected platforms
Construye sobre una base lista para producción
¿Prefieres este patrón ya implementado?
Expo Boilerplate incluye integraciones de producción, estructura de proyecto y documentación paso a paso para que te centres en tu producto.
Pago único · Actualizaciones de por vida · Reembolso de 7 días