What I learned integrating native features into a React Native app

June 18, 2026

React Native promises shared JavaScript and platform-specific native code where needed. In practice, "where needed" grows faster than most roadmaps predict. Biometrics, home screen widgets, wallet passes, push notification extensions, and OS-level deep links all pull you across the bridge.

After shipping several native capabilities in one white-label enterprise app, I stopped treating each feature as a one-off. At first, Face ID, widgets, and Wallet flows looked unrelated. In production, they all failed in similar places: release boundaries, app lifecycle, signing, and the handoff back to JavaScript.

This is the way my thinking changed after shipping those features.

The lesson I learned

Native work is never "done" when the bridge compiles. It is done when it survives release, restart, and white-label multiplication.

Before I merge a native feature now, I try to answer the same questions every time:

Phase Question to answer
Capability boundary What must run native vs what stays in JS?
Bridge contract Launch args, events, typed module API, error shapes
Build surface Which schemes/flavors need it? (multi-customer, customer-specific, QA-only)
Release type Native release required, or JS-only / OTA?
Lifecycle Cold start, background, OTA restart, logout, customer switch
Observability What to log without leaking secrets
Feature flags Per-customer rollout, QA-only paths

Those questions came from painful details, not architecture theory.

Face ID taught me that order matters

The Face ID work looked straightforward at the start. We had credentials in Keychain, a customer-level feature flag, and a clear rule: when the app foregrounds, prompt biometric unlock before letting the user continue.

The happy path worked quickly. The production path was more interesting.

The first surprise was OTA restarts. Applying a CodePush or Expo update can restart the app during a user session. If the biometric gate runs too early, the user can see Face ID more than once for what feels like a single app open.

The second surprise was cancel behavior. On iOS, LAErrorUserCancel is not the same as authentication failure. Users expected a retry screen, not a hard lockout.

The third surprise was parity. The biometric path, normal login path, session rehydration path, and QA bypass path all had to restore the same encryption state. If one path handled Keychain or encryption keys differently, customer switches could leave stale secrets behind.

The fix was mostly ordering:

// Defer biometric prompt until after OTA / deployment checks complete
await runDeploymentChecks()
if (shouldPromptBiometric() && !recentlyPromptedWithin(windowMs)) {
  await promptBiometric()
}
  • Run deployment/update checks first.
  • Prompt Face ID once per cold start, not on every intermediate restart.
  • Treat cancel as recoverable.
  • Keep Keychain access levels and encryption-key rehydration identical across login, resume, and QA paths.

That feature changed my default question from "can we call the native API?" to "where in the app lifecycle is it safe to call it?"

Widgets taught me that an extension is another app

Widgets and Live Activities looked like UI work from the product side. From the release side, they were closer to adding another app inside the app.

They required separate Xcode targets, bundle IDs, entitlements, provisioning profiles, and CocoaPods wiring. When we upgraded Xcode, extension targets broke clean builds before the main React Native app did: nested project references, missing target dependencies, and profiles that matched the main app but not the extension.

That taught me to budget for the release surface, not just the implementation:

  • Signing matrix — main app + widget + Dynamic Island + watch companion, each with Ad Hoc and App Store profiles
  • Native release only — OTA cannot patch extension binaries
  • CI time — extension builds add duplicate compile steps; Android has no equivalent here, so iOS release day is longer
  • Toolchain churn — Apple clang and Xcode updates break extension targets before they break the main RN bundle

The lesson was simple: if a feature adds a target, entitlement, or binary, it needs its own line in the release plan. Hiding it under "the app build" is how signing issues appear late.

Wallet flows taught me that native intent is not navigation

The Wallet work looked different again. We had a QR routine in JavaScript, an "Add to Wallet" handoff through a deep link or pass URL, and then a return path back into the app.

The OS part worked earlier than the product flow did. Wallet opened. The pass could be added. The problem was what happened when the app became active again.

The failure modes were all about ownership:

  • Wallet opened correctly but navigation stack was wrong on return — the user landed on home instead of the event screen they started from.
  • Universal links worked from cold start in dev but not QA builds — entitlements and associated domains differed per flavor.
  • GraphQL and UI state for the pass had to stay scoped — proof-of-concept code leaked into unrelated event queries until we isolated the module.

What helped was drawing a hard line:

  • Native, or Linking.openURL, handles the OS capability.
  • JavaScript owns the return route explicitly.
  • Test cold-start deep links on every flavor, not just the developer scheme.
  • Keep proof-of-concept routes and queries behind feature flags until routing contract is stable.

Native opens the door. The navigation model still decides whether the user lands in the right room.

The pattern I use now

The three features were different, but the review checklist converged:

Question Why it matters
Does this require a native release? OTA cannot patch native binaries, extensions, or entitlements.
Which flavors need it? White-label apps multiply signing, links, and config.
What happens on restart? OTA updates, cold starts, and background resumes can replay native gates.
What happens on customer switch or logout? Secrets, cached config, and feature flags can outlive the intended session.
Who owns the return path to JS? OS integrations deliver intents; the app still owns navigation state.

Native features in React Native are less about bridging APIs and more about owning the full lifecycle: build flavors, release boundaries, restarts, customer-specific configuration, and the handoff back to JavaScript.

That is the experience I keep coming back to. The bridge is usually not the hardest part. The hard part is making the feature behave like part of the app after the first compile: through OTA restarts, store releases, signing differences, deep links, logout, and the next customer-specific build.


Profile picture

Hi, I'm Pavel Ivanov, a staff engineer building React Native apps and AI-powered products.

I write about frontend and mobile engineering, React and React Native, platform architecture, technical leadership, and the practical side of building reliable products and teams.

Say hi on LinkedIn.

* use of site content is strictly prohibited without prior written approval