Good React Native app architecture is less about following a fashionable pattern and more about making change safe. As a codebase grows, teams usually struggle with the same issues: screens that know too much, duplicated API logic, state spread across unrelated files, and folders organized by technical type instead of product behavior. This guide explains a practical React Native app architecture that scales from a small prototype to a larger production app by combining feature folders, clear domain boundaries, and a few lightweight rules. The goal is not a perfect diagram. It is a project structure you can understand quickly, extend with confidence, and revisit as tooling, navigation, and state management choices evolve.
Overview
A useful react native app architecture should answer a few basic questions before you write much code. Where does business logic live? How do screens talk to APIs? Which files are shared across the app, and which belong to a specific feature? How do you keep navigation, data fetching, UI components, and device integrations from becoming tangled?
For most teams, the simplest durable answer is a layered, feature-first structure:
- Feature folders group code by product area, not by file type.
- Domain layers separate UI concerns from business rules and data access.
- Shared modules hold reusable utilities, design system components, and platform helpers.
- Composition at the edges wires navigation, providers, and app startup together without pushing global complexity into every feature.
This approach works well whether you use Expo or a standard React Native setup. It also stays compatible with common choices for navigation, state management, testing, and native integrations. If you are still deciding between platform setups, see Expo vs React Native CLI: Which Setup to Choose in 2026.
The main principle is straightforward: keep code close to the feature it serves, and make dependencies flow in one direction. UI can depend on use cases and services. Services can depend on API clients and storage. But your reusable domain logic should not depend on screens or navigation. Once a team follows that rule consistently, the project usually becomes easier to test, easier to onboard into, and safer to refactor.
Core framework
Here is the core model to use as a baseline react native project structure. You do not need every folder on day one, but the separation becomes valuable as soon as a second developer joins or a feature starts to branch into multiple flows.
src/
app/
navigation/
providers/
config/
bootstrap/
features/
auth/
api/
components/
hooks/
screens/
services/
state/
types/
utils/
profile/
payments/
shared/
components/
hooks/
lib/
theme/
types/
utils/
infrastructure/
api/
storage/
analytics/
notifications/
assets/
There are several reasons this works better than the older pattern of separate global folders like screens, components, hooks, and services.
First, it reduces context switching. If a bug exists in the auth flow, you can usually stay inside features/auth. Second, it lowers the risk of accidental coupling. A component used only by the payments flow should not quietly become a global dependency. Third, it makes ownership clearer. Product areas map more naturally to features than to file types.
Layer 1: App shell
The app folder should contain code that assembles the application: root navigation, provider trees, startup initialization, theme setup, and environment-aware configuration. Keep it thin. This layer should compose features, not implement them.
Typical responsibilities include:
- Registering providers for authentication, query caching, localization, or global error handling
- Mounting the top-level navigator or route structure
- Running bootstrap tasks such as loading persisted session state
- Applying theme and layout defaults
If your navigation setup is becoming hard to reason about, compare routing options in React Native Navigation Options Compared.
Layer 2: Features
Each feature folder owns a product capability. A feature may include screens, view models, local hooks, validators, API adapters, and tests. The exact internal structure can vary, but the important rule is that each feature should be understandable in isolation.
Inside a feature, a practical structure often looks like this:
- screens/: route-level components
- components/: feature-specific UI pieces
- hooks/: local state and orchestration hooks
- services/: feature-facing business logic
- api/: request and response mapping for remote data
- state/: reducers, stores, or query keys if needed
- types/: feature-specific TypeScript types
- utils/: helper logic that belongs only to that feature
This is the heart of a feature folder React Native strategy. It avoids a monolithic global store and a shared folder that turns into a dumping ground.
Layer 3: Shared modules
The shared folder is for code that is truly cross-feature. Be conservative here. Many teams move things into shared too early, then discover that every “reusable” component carries hidden assumptions from the first screen that used it.
Good candidates for shared include:
- Design system primitives like buttons, text, inputs, spacing, and layout wrappers
- Theme tokens and typography scales
- Generic hooks with no business meaning
- Date, formatting, and validation helpers used broadly across the app
- Cross-cutting TypeScript types used by many features
If a component cannot be explained without mentioning a specific product flow, keep it inside the feature.
Layer 4: Infrastructure
The infrastructure folder holds low-level integrations: API clients, secure storage wrappers, analytics adapters, push notification setup, deep linking helpers, and native bridge abstractions. This layer should expose stable interfaces that features can call without worrying about implementation details.
For example, a feature should call a notification service or analytics event helper rather than importing raw platform-specific code directly. That makes native changes easier to contain. It also helps if you later need to integrate device APIs or custom native modules. If that is part of your stack, plan for boundaries early so feature code does not become platform-bound.
Dependency direction
A scalable React Native architecture becomes much easier to maintain when dependencies move downward in a predictable direction:
- App shell depends on features and shared modules
- Features depend on shared modules and infrastructure
- Shared modules should avoid depending on features
- Infrastructure should avoid depending on feature UI
That one rule prevents many architectural problems before they start.
State and data flow
Your architecture should also define where different kinds of state live. A simple model works well:
- Ephemeral UI state: keep local with
useStateor feature hooks - Server state: manage with a query or cache layer
- Cross-feature client state: use a store only when several features truly need it
- Form state: keep near the form, not in a global store
This is one reason many teams prefer a lightweight store plus a server-state library instead of forcing all state into one pattern. For a broader comparison, see Best State Management for React Native.
TypeScript and contracts
A react native typescript setup becomes more valuable when types mirror architecture. Define contracts at boundaries: API payloads, domain models, navigation params, storage schemas, and feature-level public interfaces. Avoid leaking raw API response shapes into UI components. Instead, map them in the API or service layer and pass cleaner domain data upward.
This separation pays off whenever a backend changes field names or response nesting. Your screens stay simpler because they render what the feature needs, not whatever the server happened to send.
Practical examples
The easiest way to make architecture real is to follow one feature from the screen down to the data layer. Below are three examples that show how a scalable React Native architecture works in practice.
Example 1: Login flow
Suppose you build an authentication feature.
features/auth/screens/LoginScreen.tsxrenders inputs and buttons.features/auth/hooks/useLogin.tsmanages submission state and validation orchestration.features/auth/services/loginUser.tsdefines the business action: validate input, call repository, persist session, return result.features/auth/api/authApi.tssends the HTTP request and maps response data.infrastructure/storage/sessionStorage.tswrites tokens or user session data.
The screen should not know about token persistence details. The API layer should not decide navigation. The service coordinates the use case; the screen reacts to success or failure.
This gives you cleaner tests too. You can test the login service without rendering the screen, and test the screen without hitting the real API.
Example 2: Profile screen with shared UI
Now imagine a profile feature that shows user information, editable fields, and a reusable avatar component.
If the avatar appears in many features and has no profile-specific behavior, move it into shared/components/Avatar. But if the editable profile header contains profile-only logic, keep it inside features/profile/components.
This distinction matters because premature generalization creates hidden complexity. Shared components should be simple, stable, and broadly useful. Feature components can evolve faster because their purpose is narrow.
Example 3: Push notifications and deep links
For react native push notifications, architecture often breaks down when app startup, permission handling, token registration, and navigation side effects are mixed together.
A cleaner split looks like this:
infrastructure/notifications/handles permission requests, token retrieval, and native event subscriptions.app/bootstrap/notifications.tsinitializes listeners at startup.features/orders/services/handleOrderNotification.tsmaps payloads into product-specific actions.app/navigation/performs route navigation based on a normalized intent, not raw payload data.
That approach makes notification behavior easier to extend and less likely to break when payload formats change.
Suggested folder rules for growing teams
If your team needs concrete guardrails, these rules are usually enough:
- A feature can import from its own folder, shared, and infrastructure.
- A feature should not import internals from another feature unless a public interface is explicitly exposed.
- Shared code must have no business-specific assumptions.
- App-level files compose features but should not hold feature logic.
- Native integrations should sit behind helper modules or services, not inside screens.
These rules are simple to review in pull requests and practical enough to enforce without turning architecture into ceremony.
Common mistakes
Most architecture problems in React Native are not caused by choosing the wrong pattern name. They come from a few recurring habits.
Organizing only by technical type
A top-level folder structure like components/, screens/, hooks/, and services/ feels tidy at first, but it scales poorly. As the app grows, every change touches many folders and feature ownership becomes blurry. This is one of the main reasons teams move to a react native clean architecture style with feature grouping.
Creating a massive shared folder
Shared code should be earned, not assumed. If everything becomes shared, nothing has a clear owner. Keep code local until multiple features need it and the abstraction is stable.
Putting business logic inside screens
Screens should orchestrate UI, not contain all the rules. When validation, transformation, analytics, API calls, and navigation logic all live in a screen component, testing becomes hard and reuse disappears.
Overusing global state
Not every value belongs in a global store. Many teams reach for centralized state too early, then spend time debugging unnecessary subscriptions and stale data flows. Keep state as local as possible for as long as possible.
Ignoring platform boundaries
React Native makes cross-platform development productive, but native concerns do not disappear. Filesystem access, notifications, biometrics, media handling, and permissions all deserve clear abstraction boundaries. If screens call native modules directly, platform updates become harder to manage.
Letting navigation drive architecture
Navigation is important, but route structure should not define the entire codebase. Features may map to routes, but they may also contain background logic, reusable flows, or non-visual services. Keep navigation as a delivery mechanism, not the center of business logic.
Refactoring structure without refactoring imports and ownership
Moving files into feature folders helps only if imports and responsibilities change with them. A codebase can look feature-based while still behaving like a tangled global app. Review dependency direction, not just folder names.
When to revisit
Architecture should be revisited when the shape of the app changes, not every week. The most useful review points are practical and observable.
Revisit your structure when:
- A feature starts touching multiple unrelated folders for routine work
- New team members struggle to find where business logic belongs
- You add a major capability such as offline support, notifications, payments, or voice features
- You switch navigation, state management, or networking approaches
- You need stricter boundaries for testing, performance work, or native integrations
- The app grows from a single team project into a shared product codebase
These checkpoints often line up with broader maintenance work. If you are upgrading the stack, use that moment to review architecture assumptions too. Related reading: How to Upgrade React Native Safely and React Native Version Compatibility Matrix.
A practical review checklist looks like this:
- Map the top ten most-changed files. If they span unrelated layers, boundaries may be too loose.
- Review import patterns. Look for cross-feature reach-ins and screens importing low-level infrastructure directly.
- List duplicated logic. Repetition often reveals missing service or shared abstractions.
- Check startup complexity. If the app root knows too much about every feature, move logic back into feature modules.
- Audit state ownership. Move local state closer to screens and isolate server state from UI-only concerns.
- Define public feature interfaces. Make cross-feature use intentional instead of incidental.
If you want a simple action plan, start here this week:
- Pick one messy feature and group it into a dedicated folder.
- Extract one business action from a screen into a service or use-case function.
- Move one truly reusable UI element into shared and leave the rest local.
- Wrap one native integration behind an infrastructure helper.
- Document three import rules in your README and enforce them in code review.
That is enough to improve maintainability without pausing product work for a large rewrite.
The best React Native architecture is the one your team can apply consistently. Favor feature ownership over folder purity, clear boundaries over theory, and small refactors over dramatic resets. If you keep those priorities in place, your architecture will remain useful as tooling changes, libraries come and go, and the app grows beyond its first version.