Mobile — Mobile App Architecture

Clean Architecture for React Native Production Apps

Direct answer

Clean architecture in React Native means your business rules live in pure TypeScript modules that never import React, React Native, or any networking library. Screens and hooks sit in an outer presentation layer, API clients and storage sit in a data layer, and dependencies always point inward toward the domain. The payoff is a codebase where you can swap navigation libraries, state managers, or even the backend without touching the logic that makes the product valuable.

Most React Native apps I audit have business logic smeared across components, hooks, and API files, which makes every refactor a gamble. Clean architecture fixes that with one rule enforced consistently. This is how I structure it in real production apps without drowning the team in ceremony.

Key facts, with sources

  • React Native's New Architecture became the default in 0.76, the legacy bridge was retired in 0.82, and Hermes V1 shipped as the default JavaScript engine in 0.84. (TO THE NEW Blog)
  • Microsoft retired Visual Studio App Center and CodePush on March 31, 2025, forcing every team that depended on it to migrate their over-the-air update architecture. (microsoft/react-native-code-push GitHub issue)
  • In the State of React Native 2024 survey, Redux drew the most negative feedback at around 18% dissatisfaction, while React's built-in state management (31% positive) and Zustand (21% positive) were the best regarded. (InfoQ)
  • Over 80% of State of React Native 2024 respondents work in teams of up to five developers, meaning most mobile architectures must be maintainable by very small teams. (SSOJet (State of React Native 2024 highlights))
  • Published production examples report Shopify at 86% unified code across its apps and Instagram sharing 85 to 99% of code between iOS and Android. (CatDoes)

The dependency rule is the whole game

Clean architecture gets dressed up in diagrams, but in a React Native codebase it reduces to a single enforceable rule: source code dependencies point inward. The domain layer imports nothing from React, React Native, axios, or AsyncStorage. The data layer imports domain types but not screens. Screens import hooks, hooks import use cases. That is it.

When I review a codebase, I check this mechanically: grep the domain folder for framework imports. If a pricing calculation imports a component, or a validation rule reads from a Zustand store directly, the architecture has already failed regardless of how the folders are named. Everything else in this post is scaffolding to make that one rule easy to follow under deadline pressure.

The domain layer: pure TypeScript, zero framework imports

The domain layer holds entities, value objects, and use cases — the code that would survive a rewrite to Flutter or native. I define repository interfaces here too, so the domain declares what it needs without knowing how it is fulfilled. A use case is often just a curried function that accepts its dependencies and returns an async function.

This layer is where unit tests earn their keep. Because nothing here touches React or the network, tests run in plain Node with no mocking gymnastics. In practice I find teams that adopt this pattern write far more tests, simply because writing them stops hurting.

Use case with an injected repository interface
// domain/orders/submit-order.ts — pure TypeScript, no React imports
export interface OrderRepository {
  getOpenOrders(): Promise<Order[]>;
  submit(draft: DraftOrder): Promise<Order>;
}

export const makeSubmitOrder =
  (repo: OrderRepository) =>
  async (draft: DraftOrder): Promise<Order> => {
    if (draft.items.length === 0) {
      throw new EmptyOrderError();
    }
    return repo.submit(draft);
  };

The data layer: implement the interfaces, own the mess

The data layer is where HTTP clients, response mappers, caching, and offline persistence live. It implements the repository interfaces the domain declared. Crucially, API response shapes stay inside this layer — a `mapOrderDto` function converts wire formats into domain types at the boundary, so a backend field rename becomes a one-file change instead of a codebase-wide search.

This is also where I put the ugly pragmatic code: retry logic, token refresh, response envelope unwrapping, feature-flag-driven endpoint switches. Concentrating the mess here keeps it out of components. When the backend team ships a v2 API, you write a second repository implementation and flip them behind the same interface.

Presentation: thin screens, hooks as the seam

Screens should read like a table of contents: call a hook, render states. The hook is the seam between React and the domain — it wires a use case to a concrete repository, manages loading and error states, and hands the screen plain data. I keep hooks under roughly fifty lines; when one grows past that, logic is usually leaking in that belongs in the domain.

A useful smell test: can you describe what a screen does without mentioning any business rule? If the checkout screen knows the tax rounding policy, that policy will eventually be duplicated in the order-history screen and the two will drift. Push it down and both screens call the same function.

Where teams over-engineer this

Clean architecture fails in React Native when teams import the full enterprise version: five layers, mapper classes for every type, dependency-injection containers, interfaces for things with exactly one implementation. A CRUD screen that fetches a list and renders it does not need a use case — a React Query hook calling a typed API function is genuinely fine.

My rule of thumb: introduce the domain layer for code with real business rules — pricing, permissions, validation, sync conflict resolution — and let simple read-only screens talk to the data layer through hooks directly. Architecture is a budget. Spend it where logic is complex and volatile, not uniformly across the app because a diagram said so.

When to hire senior help

Architecture is the cheapest place to buy senior expertise, because decisions about state management, navigation, offline strategy, and update infrastructure made in week one determine costs for years. A short engagement with a senior mobile architect before or during MVP planning routinely prevents the rewrite-at-scale scenario that hits teams around their first major growth phase. If your stack includes React Native + Python + AI, a senior engineer who owns the full product beats coordinating multiple juniors.

Bottom line

Dhairya Senjaliya ships Mobile — Mobile App Architecture projects worldwide — book a scoping call to discuss your specific situation.

Common pitfalls to avoid

  • Adopting Redux with sagas and heavy boilerplate for a five-screen MVP when built-in React state or Zustand would cover the actual requirements
  • Scattering business logic inside UI components instead of isolating a data layer, making later backend changes or native module swaps expensive
  • Building deployment architecture on a hosted OTA service with no exit plan, a risk the March 2025 CodePush shutdown made concrete for thousands of teams
  • Assuming permanent connectivity and bolting on caching later, instead of designing offline storage and sync conflict resolution before the data layer hardens

Frequently asked questions

Is clean architecture overkill for a small React Native app?

The full layered version, yes. But the core rule — keep business logic in pure TypeScript files that never import React or networking libraries — costs almost nothing and pays off within weeks. I apply that rule even in MVPs, and add explicit use cases and repository interfaces only once real business rules like pricing or permissions appear.

Does clean architecture work with Expo and React Query?

Yes, they compose well. React Query manages server-state caching in the presentation and data layers, while your domain layer stays pure TypeScript that neither library touches. Expo changes nothing about the pattern — it affects the native build layer, not how you organize JavaScript. I ship this combination as my default stack for production Expo apps.

How do I test a clean architecture React Native app?

Test the domain layer with plain unit tests in Node — no React, no mocks beyond in-memory repository fakes, so tests are fast and stable. Test hooks with React Testing Library where the wiring is nontrivial, and reserve a small set of end-to-end tests for critical flows. Most of your coverage should land in the cheap domain tier.

What state management should a new mobile app use?

For most apps, React's built-in state plus a light library like Zustand is enough; in the State of React Native 2024 survey those two drew the most positive sentiment while Redux drew the most negative at about 18% dissatisfaction. Heavier tooling is justified mainly by large teams, complex shared state, or strict audit requirements.

Do we need offline support from day one?

If users operate in the field, in transit, or in markets with unreliable networks, yes, because retrofitting offline-first sync onto an online-only data layer is one of the most expensive refactors in mobile. If the app is unusable without live data anyway, graceful error and retry handling may be sufficient.

What are over-the-air updates and should our app use them?

OTA updates push JavaScript-level fixes directly to users without waiting for app store review, which is valuable for hotfixes. Microsoft's CodePush was retired on March 31, 2025, so current options are EAS Updates, a self-hosted CodePush server, or third-party services, and updates must stay within store policies that prohibit changing an app's core purpose.

Bottom line: Dhairya Senjaliya ships Mobile — Mobile App Architecture projects worldwide. Book a scoping call at https://dhairyasenjaliya.com/#book-call.

Sources

Related guides

Keep up with new guides

New deep-dive guides on React Native, Python, and AI ship regularly. Subscribe via RSS or follow on LinkedIn.

Want help implementing this?

30-minute scoping call · Clear milestones · Senior engineer ownership