A decision guide for engineering leaders covering modules, state, release design, and migration from a monolith
React Native’s old architectural bottleneck was easy to identify. JavaScript communicated with native code through an asynchronous, JSON-serialized bridge, and the cost often showed up as laggy gestures, dropped frames, or uneven scrolling.
That is no longer the main architectural question. React Native 0.82, released in October 2025, became the first version to run exclusively on the New Architecture. The opt-out disappeared. Fabric, JSI, TurboModules, and Codegen are now the baseline rather than choices teams debate at the start of a project.
Large React Native apps still become difficult to change, but for different reasons. Features cross boundaries that were never clearly defined. Server data gets mixed into global UI state. A shared navigator grows until every team needs to edit it. One abandoned native dependency blocks the next framework upgrade.
A scalable React Native app architecture has to control those costs as the product, codebase, and engineering organization grow. The right structure depends less on the number of users than on how many features, teams, platforms, and release paths the application must support.
1. What scalability means in a mobile application
“Scalable” is often treated as shorthand for “capable of serving millions of users.” That definition fits backend infrastructure better than a mobile client. The backend absorbs traffic; the application absorbs product complexity.
For architecture decisions, it helps to separate four kinds of scale:
Traffic and data volume: how much data the app requests, caches, processes, and keeps in memory. Much of this is a backend, pagination, and caching concern.
Feature surface: the number of screens, workflows, integrations, and product areas contained in one application.
Contributing teams: how many engineers and teams work in the same codebase without repeatedly blocking one another.
Target surfaces: the number of environments supported from the same codebase, including phones, tablets, foldables, web, TV, and XR devices.
Most React Native applications described as “unscalable” are struggling with feature surface and contributor count, not raw traffic. Organizations such as Shopify, Microsoft, and Meta use React Native at scale, but the difficult part is coordinating development across a large product rather than handling requests from individual devices.
The symptoms can be measured. Rising merge-conflict rates and longer pull-request queues point to team coupling. Time-to-first-commit for a new engineer shows how much of the system someone must understand before making a safe change. Regression frequency, cold-start creep, and the number of files touched by routine work expose structural decay.
Reframe
Scalability is not an inherent property of React Native. It is the cost of changing the application as the product grows. If a one-line copy update takes a day to release safely, the architecture is failing even if the renderer remains fast.
2. The 2026 baseline: what the New Architecture resolved
JSI, Fabric, TurboModules, and Codegen replaced the old asynchronous bridge model. The New Architecture became the default in React Native 0.76. Starting with version 0.82, setting newArchEnabled=false on Android or disabling it during the iOS CocoaPods installation no longer has any effect.
Expo followed the same direction. According to its New Architecture documentation, SDK 54 was the final version that allowed teams to disable it. SDK 55 uses React Native 0.83, removes the meaningful opt-out, and begins moving further away from the legacy implementation. The old runtime was frozen in June 2025, so it receives neither new features nor bug fixes.
For applications still running on older versions, this creates an upgrade ceiling. A library that has never been updated for the New Architecture is no longer a minor maintenance concern; it can prevent the application from reaching a supported React Native release.
Compatibility layers still allow many older native modules to operate. The React Native team has said those interop layers will remain for the foreseeable future, so their removal is not an immediate threat. They should still be treated as a migration aid, not the desired long-term state for a critical dependency.
What changed underneath the framework does not solve the structural problems above it. An application with one 400-file navigator, uncontrolled cross-feature imports, and server data copied into a global store remains expensive to modify after migration. The rendering pipeline may be newer. The coupling is not.
Sourcing note
Figures such as “43% faster cold start” and “40× faster JavaScript-to-native calls” appear across vendor articles without a consistent, traceable benchmark. The New Architecture can improve animation-heavy and scroll-heavy workloads, but performance percentages are only meaningful when the tested application, device, build type, and measurement method are disclosed.
3. The Modularity Ladder
“Use feature folders” is sensible advice for a small application. It stops being sufficient once multiple teams need ownership and release independence.
Modularity is better treated as a ladder. Each tier addresses a particular form of coordination pressure and introduces costs of its own. Moving higher before those pressures exist usually gives the team more infrastructure without reducing the cost of change.
AxTieris
Suitable context
Trigger for moving up
Setup cost
Reversal
Common failure
Tier 1: Feature folders in one app
One team and roughly 40 screens
Repeated cross-feature edits and unclear ownership
Cross-imports gradually turn folders into cosmetic boundaries
Tier 2: Enforced internal boundaries
Recurring merge conflicts and repeated questions about ownership
Path aliases, import rules, public APIs, ownership files, and review discipline
Rules are documented but not enforced in CI
Tier 3: Monorepo workspace packages
Three to six teams, multiple apps, or white-label products
Several apps share code and version skew appears
Build orchestration, CI caching, package versioning, and dependency governance
Slow uncached builds and uncontrolled dependency drift
Tier 4: Runtime composition
Separate product organizations that genuinely require independent JavaScript releases
Teams cannot ship on the same schedule
Release coordination, singleton constraints, and host-binary planning
Version-skew failures and native capabilities missing from the host
Tier 2 may include path aliases, forbidden-import rules, a public entry point for each feature, and CODEOWNERS files. Tier 3 moves those boundaries into workspace packages using pnpm, Nx, Turborepo, or similar tooling. Shared design systems and internal libraries can then be versioned and consumed by several applications.
Tier 4 includes Module Federation through Re.Pack or a super-app composed of separately delivered mini-apps. It is the most frequently misunderstood tier.
A runtime remote can deliver JavaScript, not native code. Re.Pack’s Module Federation documentation requires react and react-native to be configured as eager singletons in every container. The installed host binary must also contain every native module needed by a remote. A mini-app cannot add a new native SDK to an application that users have already installed.
Callstack therefore describes runtime delivery as dynamic JavaScript operating within mobile-store rules, not a way around those rules.
Anti-pattern: skipping tiers
Many teams considering Module Federation need Tier 2 instead: enforced boundaries in a single repository. Federation becomes worthwhile when separate organizations truly need independent JavaScript release schedules.
Downgrade signals matter too. If every remote always ships with the host, runtime composition is not buying release independence. If monorepo CI takes longer than the coordination it saves, the workspace may have been introduced too early.
4. A module still needs an internal structure
A feature folder is not a meaningful boundary if screens, API calls, storage code, and business rules can all import one another.
A practical module can be divided into three layers:
Presentation: screens and UI components. This layer renders data and handles interaction but does not contain core business rules.
Domain: hooks, services, selectors, and application logic. It should not depend on JSX or rendering details.
Data: API clients, repositories, local storage adapters, and typed interfaces to external systems.
The important part is dependency direction. Presentation may call into the domain layer. The domain layer may use interfaces implemented by the data layer. Data code should never import screens or presentation components.
Other modules should see only a deliberately small public API: routes, a provider where one is needed, and typed events or commands. Internal stores, hooks, and components stay behind that entry point.
This does not require a dependency-injection container or a repository abstraction for every API request. An application with one data source may gain little from that ceremony. The purpose of layering is to limit the direction in which dependencies can spread.
There is a simpler test: can the feature be removed in one pull request? If deleting it requires edits in 15 unrelated files, the module boundary exists mostly in the directory tree.
Public React Native codebases such as Bluesky and established starter projects such as Ignite are useful references. They are better starting points than designing a convention around an abstract architecture diagram.
5. State ownership matters more than the library
Redux versus Zustand is a reversible decision. Misclassifying state is not.
A scalable application distinguishes between data received from a server, local interface state, persisted device data, and short-lived form state. Each has different ownership, persistence, and invalidation requirements.
State type
Correct location
Typical tools
Persistence
Invalidation owner
A query cache, preferably scoped by feature
TanStack Query or RTK Query
Component state or an in-memory feature store
Zustand, Context, or Redux Toolkit
MMKV, SQLite, or WatermelonDB
Local component or form state
React Hook Form or useState
The recurring failure is copying server responses into a global client store. Once that happens, the team starts rebuilding caching, retries, request de-duplication, invalidation, and stale-data handling by hand. Those implementations tend to work until several screens modify or depend on the same data.
Server data belongs in a query cache built for those problems. A global client store should be reserved for state the application itself owns.
The State of React 2025 results show TanStack Query leading data-fetching usage, while many respondents report using no dedicated global-state library. React hooks combined with a query cache cover more cases than teams often assume. The State of React Native survey now directs state-management and data-fetching questions to the broader React survey.
Offline-first behavior changes the decision. It introduces a synchronization queue, optimistic writes, conflict resolution, background retries, and local schema migrations. These are product and data-consistency choices, not options that can be enabled later without redesigning the application.
State should generally remain owned by the module that uses it. The library can be replaced. Recovering ownership after every feature depends on one shared store is much harder.
6. Navigation is a public contract between modules
The root navigator is often the most heavily contested file in a growing React Native application. If every new screen requires editing one central switch statement, feature teams are not independent regardless of how neatly the rest of the code is divided.
Modules should contribute their own route subtrees through a stable registration contract.
Four decisions deserve explicit treatment:
Route registration: Allow modules to define and expose their route trees rather than listing every screen in one central file. React Navigation 7’s static API improves TypeScript and deep-linking support for statically configured navigators. Expo Router reaches a similar result through file-based routing.
Deep and universal links: Treat links as versioned external interfaces. Emails, notifications, websites, and partner systems may depend on paths long after internal screens have been reorganized.
Authentication and permissions: Apply access rules at the navigator level through protected routes or a gated navigator. Reimplementing the same checks inside individual screens creates gaps.
Typed route parameters: Route parameters are cross-module interfaces. A feature should consume generated or exported types instead of importing another module’s internal screens.
Expo Router and bare React Navigation impose different constraints. Expo Router maps routes to files, which gives teams a standardized structure and straightforward deep linking. Bare React Navigation allows more explicit control over dynamically assembled navigators.
That distinction becomes more relevant at Tier 3 and Tier 4, where packages or runtime modules may contribute route trees. For a conventional application, file-based routing is often the simpler contract. For dynamically composed applications, explicit registration may be easier to govern. A detailed navigation comparison can help teams evaluate the trade-off.
7. Treat native libraries as governed dependencies
A third-party native library is not merely a package choice. It can determine whether the entire application can upgrade.
Each native dependency should be evaluated along two axes: maintenance status and New Architecture support.
Compatibility status
Maintained
Unmaintained
Rewritten for TurboModules or Fabric
Keep. This is the preferred position for libraries such as Reanimated, MMKV, and Screens.
Rare but still fragile. Fork it or plan a replacement.
Legacy implementation running through interop
Usable for now, but the migration should have an owner.
Highest risk. Fork, replace, or remove it before it blocks an upgrade.
The ecosystem has moved substantially. Expo reports that many popular packages now support the New Architecture, and npx expo-doctor checks project dependencies against the React Native Directory. It can identify packages that are incompatible, unmaintained, or not yet assessed.
A useful production triage is compatible, interop, or incompatible. An incompatible dependency requires a fork-or-replace decision. Waiting without an owner or deadline only postpones the same problem.
Custom native modules should be the fallback when no maintained alternative exists. Teams can define a TurboModule through a TypeScript specification and Codegen or use the Expo Modules API. Either route creates a permanent maintenance obligation across React Native, iOS, and Android upgrades.
A manageable governance model includes:
A short architecture decision record for every new native package.
An approved-library list.
A named upgrade owner for each critical dependency.
A replacement plan for packages that run only through interop.
Brownfield applications need one more rule. When React Native is embedded inside an existing native app, keep the boundary small and explicit. Native-owned code and React Native-owned code should communicate through a narrow interface rather than importing freely across the seam.
8. Release architecture: OTA updates, flags, and store binaries
Release design determines how independently teams can deliver changes. The first constraint is fixed: JavaScript and assets may be updated over the air, while native code requires a new store binary.
Change
Delivery path
Rollback speed
Constraint
JavaScript logic, copy, styles, and most UI changes
OTA through Expo Updates or a self-hosted service
Must not change the application’s advertised purpose
A feature already included in the reviewed binary and controlled by a flag
Immediate through a kill switch
Must not expose undisclosed functionality
A native module, SDK, or permission
A breaking change across native and JavaScript layers
New binary plus a forced-update or compatibility policy
Native and JS versions must remain compatible
Apple’s rules permit interpreted code within defined limits, but OTA updates cannot change the app’s primary purpose, bypass the sandbox, or reveal functionality hidden from reviewers. Google Play restricts downloaded executable code while treating JavaScript bundles separately from changes to the native binary. The relevant distinction is explained in analyses from Bitrise and OTAkit.
Feature flags combined with trunk-based development provide much of the release independence teams seek from runtime module composition, with far less operational machinery. Features can be merged, deployed gradually, and disabled without maintaining separately loaded application containers.
Two protections are still necessary:
-
Use staged rollout and a kill switch for high-risk changes.
-
Bind each OTA bundle to a compatible native runtime version.
The second point prevents an OTA update built for a new native SDK from loading on an older installed binary. Without runtime-version gating, JavaScript and native code can drift into combinations that were never tested together.
9. Make performance and structure enforceable
Performance rarely collapses in one dramatic release. It decays through small additions: another synchronous initialization task, a larger bundle, a screen that renders more often, or a dependency that quietly increases startup work.
That is why performance targets need to be CI checks tied to a named baseline device.
Metric
Baseline
Enforcement
Response to a breach
Cold start and time to interactive
Automated device benchmarking
Block the change or create an owned regression task
Fail above the agreed threshold
Component render performance
Recorded render measurement
Investigate statistically meaningful changes
Frame drops and FPS on critical flows
Fixed scenarios on a named device
Block release below the score floor
Production latency and crashes
Sentry, Datadog, or equivalent RUM
Alert and assign triage ownership
Flashlight produces an Android performance score based on CPU, frame rate, and memory. It works against production builds without requiring instrumentation inside the app and can run in CI.
Reassure adds render-performance regression testing to a Jest and Testing Library setup. It uses repeated runs and statistical analysis rather than treating every small timing difference as a regression; changes below roughly 5% are generally treated as noise.
Startup thresholds should come from measurements on a real low-end target device and platform launch-time guidance. A universal millisecond target copied from another application has little value if the build type, hardware, and startup path differ.
Performance checks cover only half the problem. Architectural fitness functions can also run in CI:
Forbidden-import rules can prevent presentation code from importing the data layer directly.
Dependency-graph checks can reject new cross-module edges.
Ownership checks can require approval from the team responsible for a module.
Package-boundary rules can stop applications from importing internal files instead of public entry points.
A failed check must lead to an explicit decision: revert the change, accept the new baseline, or track the regression with an owner. An alert that nobody is responsible for becomes background noise.
10. Align module ownership with team ownership
Conway’s Law is usually quoted as a general observation: software reflects the communication structure of the organization that produced it. In a React Native application, the practical consequence is direct. If three teams own code inside the same large module, every release requires cross-team coordination. If one feature spans several ownership groups, architectural work turns into scheduling work.
A workable structure separates platform responsibilities from vertical product ownership.
The mobile platform team owns the application shell, navigation contract, shared design system, CI infrastructure, release pipeline, and framework upgrades. Its job is to make feature delivery easier, not to approve every feature change.
Feature teams own vertical slices from presentation through domain logic and data access. Their responsibility ends at a documented module boundary.
The benefit becomes obvious during onboarding. A new engineer should be able to work safely inside one module without first understanding the entire application. That same property makes additional engineering capacity easier to use. Teams that hire React Native developers for a specific product area can give them a bounded module and a clear public API instead of opening the whole codebase at once.
The supporting artifacts are ordinary but necessary: CODEOWNERS, a short README for each module, and an architecture decision log explaining why the boundaries exist.
11. Migrating a React Native monolith without rewriting it
A rewrite replaces known defects with unknown ones while product development continues around it. Most React Native monoliths are better handled through incremental extraction: build the new structure around the existing application, then retire old paths as features move.
The order matters.
1. Upgrade before reorganizing
Move to a supported React Native version and resolve every library blocking the New Architecture before changing module boundaries. Refactoring an application that cannot be upgraded leaves the highest-risk problem untouched.
The React Native team recommends migrating through the final dual-support release: React Native 0.81 or Expo SDK 54. Enable the New Architecture there, resolve compatibility problems, and only then move to 0.82 or later. The official Upgrade Helper provides the native project changes between versions.
2. Measure the monolith
Generate a dependency graph before deciding where modules should sit. Combine it with file churn and contributor count.
Files with both high churn and many contributors are coordination hotspots. They may not be the first files to extract, but they show where unclear ownership is most expensive. Module cohesion, inbound dependencies, test coverage, and native integrations should also be recorded.
3. Extract a leaf module first
The most painful feature is rarely the safest starting point. Choose a module with few inbound dependencies and a clear product boundary.
A low-risk extraction tests the proposed structure, import rules, ownership model, and CI checks. If the design is wrong, the team can adjust it before moving a central workflow.
4. Freeze the new boundary immediately
Once a seam exists, protect it in CI. Documentation alone will not stop convenience imports during a deadline.
Add forbidden-import rules, expose a public entry point, assign ownership, and reject new dependencies that cross the boundary without an explicit decision.
5. Promote only when a trigger appears
Move from folders to enforced boundaries when ownership and merge conflicts become recurring problems. Adopt a monorepo when several applications genuinely need versioned shared packages. Consider runtime composition only when separate product organizations cannot release on the same schedule.
The migration plan should also say what will not be moved. Without that limit, incremental modernization turns into an open-ended cleanup program.
Fixed claims such as “a React Native migration takes two to eight weeks” hide the variables that determine the work. Custom native-module count, abandoned dependencies, test coverage, brownfield integration, and release constraints matter more than the number of screens. One field estimate places custom-module migration at roughly one or two days per module, but even that should be treated as an initial planning input, not a universal rate.
Teams without mobile-platform expertise sometimes bring in experienced React Native development companies to handle the upgrade and initial extraction alongside the existing product team. The value is concentrated in the early decisions: clearing native blockers, choosing a viable boundary, and preventing the new structure from inheriting the monolith’s coupling.
Where to start
For current React Native versions, the framework architecture is no longer optional. The remaining decisions concern how expensive the application will be to change after more features and teams arrive.
Identify the application’s current tier before adopting new infrastructure. Then measure the symptoms that would justify moving higher: merge conflicts, PR wait time, onboarding time, dependency skew, regression frequency, and release coordination.
An old React Native version changes the order of work. Upgrade first. Replace or migrate the blocking native dependencies. After the application runs reliably on the New Architecture, extract one low-risk module and enforce its boundary. That first stable seam is more valuable than a large target-state diagram nobody can implement safely.
Frequently asked questions
Is the New Architecture mandatory?
Yes, on current versions. React Native 0.82 removed the opt-out, and Expo SDK 55 runs entirely on the New Architecture. Expo SDK 54 and React Native 0.81 were the last releases that allowed teams to disable it.
Interop layers still support many legacy native modules, but the old runtime is frozen and there is no supported path back on newer releases.
Should a large multi-team app use Expo or bare React Native?
Expo is a reasonable default in 2026. Development builds provide native access, EAS supports builds and OTA delivery, and Expo Router standardizes navigation.
Bare React Native is more appropriate when the application requires custom native integration that Expo’s plugin system cannot express or when React Native is embedded deeply inside an unusual brownfield host.
Does every large React Native application need a monorepo?
No. A monorepo starts to pay off when multiple applications or white-label builds share versioned packages. Two or three teams working in one app often gain more from enforced internal boundaries than from adding workspace and build-orchestration infrastructure.
When is Module Federation justified?
Use it when separate product organizations must deliver JavaScript on independent schedules.
It introduces singleton constraints, release coordination, and version-skew risks. It also cannot deliver native modules dynamically. If all teams already release together, enforced repository boundaries or a monorepo will usually be simpler.
Can React Native support a super app?
Yes. Re.Pack and Module Federation can support independently delivered JavaScript mini-apps.
Every native capability those mini-apps require must already exist in the host binary. JavaScript can be loaded dynamically; the native surface cannot.
How can React Native be added to an existing native app?
Use a brownfield integration and start with one self-contained screen or workflow. Define a narrow interface between the native-owned and React Native-owned parts of the application, and prevent either side from importing internal implementation details from the other.
Expand the React Native surface only after that boundary has proved stable.
What should a React Native architecture review cover?
A useful review examines:
The current React Native version and upgrade blockers.
Module ownership and cross-import leakage.
The split between server cache and client state.
Navigation and deep-linking contracts.
Native-dependency health and ownership.
OTA delivery, feature flags, and runtime-version compatibility.
Performance budgets and structural rules enforced in CI.
The relationship between module boundaries and team responsibilities.