Mobile — Mobile App Architecture

React Native Navigation Patterns for Complex Apps

Direct answer

For complex React Native apps I use React Navigation's native stack with a single root navigator that composes three concerns: an auth switch rendered conditionally, a tab navigator holding per-tab stacks, and modal flows registered at the root so any screen can open them. Param lists are fully typed, deep links are defined in one linking config, and screens never receive business objects through params — only IDs.

Navigation is the skeleton of a mobile app, and a bad skeleton makes every feature harder to hang. Most navigation pain in the codebases I rescue comes from structure invented screen by screen instead of designed once. These are the patterns that keep a forty-screen app navigable — for users and for the team.

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)

One root navigator, three concerns

My root layout is boring on purpose: a native stack whose contents are chosen by auth state. Signed out, it renders the auth group; signed in, it renders the main tab navigator plus a set of root-level modal screens. The auth switch is conditional rendering of screen groups, not imperative navigation — when the session ends, the signed-in screens unmount and there is no way to back-navigate into them.

Each tab owns its own stack, so Home and Orders keep independent histories, matching what users expect from native apps. The root stack exists above the tabs specifically to host flows that must cover the tab bar: checkout, full-screen media, and anything modal. Resist the urge to add navigators beyond these three concerns; every extra layer multiplies edge cases in back-button behavior.

Type the param lists, pass IDs not objects

Typed param lists turn navigation typos into compile errors, which matters when forty screens navigate into each other. The other rule I enforce: params carry identifiers and small primitives, never full business objects. Passing a whole order object through params creates a stale copy the moment the underlying data changes, and it breaks deep linking because a URL cannot carry your object.

The target screen takes an ID and fetches from the cache — with React Query the data is usually already there, so there is no visible loading cost. This one convention eliminates a whole category of "screen shows outdated data" bugs.

Typed root and tab param lists
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import type { NavigatorScreenParams } from '@react-navigation/native';

export type TabsParamList = {
  Home: undefined;
  Orders: undefined;
};

export type RootStackParamList = {
  Tabs: NavigatorScreenParams<TabsParamList>;
  OrderDetail: { orderId: string };
  // Modals live at the root so any screen can open them
  RateOrder: { orderId: string };
};

const Stack = createNativeStackNavigator<RootStackParamList>();

Modal flows belong at the root

Multi-step flows — checkout, onboarding upsells, KYC — should be their own stack presented modally from the root, not screens spliced into a tab's history. Nesting a five-step checkout inside the Home stack means the back button walks users backward through the funnel one step at a time, and abandoning the flow leaves debris in the history.

As a root-level modal group, the flow has a clean lifecycle: present it from anywhere, and one dismiss removes the entire thing. Internal steps get their own nested stack so in-flow back behavior still works. This also simplifies analytics — flow entry and exit are single navigation events — and makes the flow reusable from multiple entry points without duplicating wiring.

Deep linking as a first-class design input

Deep links bolted on late force painful restructuring, because a link must express a full navigation state — tab, stack, params — and if your hierarchy cannot represent "Orders tab, order detail, review modal" as a path, you rebuild it. I write the linking config early, even before marketing asks for it, because push notifications need the same machinery: a notification tap is just a deep link with a different entry point.

Keep one linking configuration as the single source of truth mapping URL paths to screens, and validate incoming params at the boundary — links arrive from outside your type system, so treat their params as untrusted input and fall back to a safe screen when parsing fails. Cold-start links deserve explicit testing; that path traverses your entire startup sequence and breaks most often.

Navigation performance and state hygiene

Two performance defaults: use the native stack rather than the JS stack so transitions run on native primitives, and keep tabs lazy so the app does not mount four navigators at launch. Heavy screens should defer expensive work until after the transition completes — interaction-aware scheduling keeps push animations at full frame rate.

State hygiene matters as much: navigation state is not application state. Do not mirror "current screen" into a global store, and do not fire navigation as a side effect of store changes scattered around the codebase — both create loops that are miserable to debug. The auth switch is the one sanctioned place where state drives navigation structure. Everything else navigates explicitly from user actions, which keeps the flow of control readable.

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

Should I use Expo Router or plain React Navigation for a complex app?

Expo Router is built on React Navigation, so the underlying patterns are identical — file-based routes generate the navigators. For new Expo projects I generally take Expo Router because deep linking comes nearly free. For existing apps with hand-built navigators, migrating rarely pays for itself; the structure and typing discipline matter far more than which flavor you use.

How do I handle navigation after login in React Native?

Do not navigate imperatively after login. Render different screen groups from the same root navigator based on session state: signed-out screens when there is no session, the main app when there is. When the session state flips, React Navigation swaps the groups, the transition happens automatically, and signed-in screens cannot be reached by back-navigation after logout.

Why should navigation params contain IDs instead of full objects?

Objects in params become stale copies the moment the source data updates, causing screens to show outdated information. IDs keep one source of truth: the target screen fetches from your cache, usually instantly. IDs also serialize cleanly, which deep links, state persistence, and notification payloads all require. Full objects break every one of those paths.

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