Architecture 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 with feature slices, not microservices: Colocate screens, hooks, and components per product area in features/<name>/ - evolve structure with team size, not day-one distributed systems.
Keep Expo Router routes thin: app/**/*.tsx re-exports feature screens and parses params - business logic belongs in src/features/, not route files.
Expose a public API per feature: Curate features/<name>/index.ts - sibling features import the barrel, never deep paths into components/ or model/.
Respect inward dependency flow: app → features → entities → shared - shared code never imports features; entities never import features.
Put shared primitives in shared/ui: Generic Button and Screen live in shared; domain-specific UserAvatar lives in entities/user/ui.
Model nouns in entities: Plain types and pure functions for User, Order - zero React imports, testable in Node without a renderer.
Hide IO behind adapters: createOrdersApi(client) and createSecureTokenStorage() - screens and use cases never call fetch or SecureStore directly.
Add use cases when tests need seams: Extract signIn, placeOrder use cases when hooks mix IO, branching rules, and UI state - not for every button toggle.
Return discriminated unions from use cases: { ok: false, error: "network" } beats thrown errors across layer boundaries - callers handle failures explicitly.
Wire dependencies at the composition root: AppServicesProvider in app/_layout.tsx binds production adapters - avoid module-scope singletons.
Prefer factory injection over DI frameworks: createX(deps) plus React context covers mobile needs - skip tsyringe/Inversify unless org mandates it.
Default to TanStack Query for server state: Lists, details, and mutations belong in Query cache - not Redux or Zustand slices duplicating API data.
Use Zustand for small global client state: Cart, preferences, and UI chrome - selectors prevent list re-render storms; not for server JSON blobs.
Keep ephemeral UI state local: Sheet open state and accordion index stay in the screen tree until a second unrelated route genuinely needs them.
Put URL state in Expo Router params: Filters and tabs via useLocalSearchParams - shareable deep links beat global store keys.
Document navigation choice in an ADR: Greenfield SDK 57 apps default to Expo Router; brownfield React Navigation stays valid - record the decision for onboarding.
Map FSD layers pragmatically: app/, features/, entities/, shared/ - skip widgets/ and pages/ until a composite block repeats across routes.
Enforce boundaries with ESLint: no-restricted-imports blocking @/features/*/components/* - folder conventions alone fail at 10+ features.
Modular monolith before multi-app: One repo with APP_VARIANT and feature flags until bundle IDs, compliance, or release cadence truly diverge.
Separate EAS Update channels per variant: production-consumer vs production-business - wrong channel updates the wrong store listing.
Centralize config in getAppConfig(): Read Constants.expoConfig?.extra once - features do not scatter process.env and __DEV__ branches.
Orchestrate cross-feature flows at routes: Pass onAddToCart callbacks from route files - features do not import sibling feature stores.
Run the refactoring checklist before RC: Audit top five files by line count and import fan-out - god modules block parallel PRs and SDK upgrades.
Extract incrementally, one boundary per PR: Move one hook or component per merge - abandon multi-week "cleanup" branches that diverge from main.
Match architecture investment to risk: Auth, payments, and offline sync earn ports and use cases - three-screen MVPs earn hooks and an API module until complexity proves otherwise.
When you need swappable IO (mock/staging/prod), compliance unit tests on business rules, or multiple adapters per feature - not on day one of a CRUD app.
Default for greenfield apps. Brownfield React Navigation remains supported - migrate when deep linking pain exceeds migration cost.
More than Query + one client store (Zustand or Context) warrants an audit. Server data should not live in client stores.
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