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

  1. Project boundaries
  2. TypeScript and validation
  3. Components and hooks
  4. State and data fetching
  5. Errors and observability
  6. Performance
  7. Accessibility
  8. Security
  9. Testing and delivery
  10. 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.ts files 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 unknown instead of any for 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 use and 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:

StateRecommended owner
Remote API dataTanStack Query
Form input and validationForm component or form library
Temporary UI stateNearest owning component
Cross-feature session or themeFocused context/provider
Persisted preferencesStorage adapter plus a typed hook
Navigation stateExpo 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 FlatList or 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