The Complete Guide to App Updates and Versioning in React Native

Managing app updates and versioning is one of the most critical yet often underestimated aspects of maintaining a production React Native application. A well-structured update strategy ensures users always have access to the latest features, critical security patches, and performance improvements—without disrupting their experience or causing unexpected downtime. However, the hybrid nature of React Native (JavaScript bridge or new architecture with JSI, native modules, and platform-specific binaries) introduces unique complexities that demand a deliberate approach.

This guide covers the full spectrum of update management: from semantic versioning and over-the-air deployments to App Store submissions, automation, rollback strategies, and testing. You will learn how to build a robust, user-friendly pipeline that balances speed of delivery with stability.

Understanding App Versioning in React Native

Versioning in React Native involves maintaining a clear, auditable record of every release. The system typically uses two identifiers: the version number (human-readable) and the build number (machine-incrementing integer). These identifiers serve multiple purposes: they help users identify which release they have, enable developers to correlate crash reports and bug reports to specific builds, and provide the App Store and Play Store with the data needed to manage staged rollouts and update notifications.

Semantic Versioning (SemVer)

The overwhelming industry standard is semantic versioning, following the format MAJOR.MINOR.PATCH (e.g., 2.1.0). The rules are straightforward:

  • MAJOR incremented when you introduce breaking changes that require users to behave differently or that alter data formats, APIs, or key integrations.
  • MINOR incremented when you add functionality in a backward-compatible manner, such as a new screen, feature flag, or improved UX flow.
  • PATCH incremented for backward-compatible bug fixes, security patches, and minor performance tweaks.

React Native projects store these values in multiple locations: app.json (for the JavaScript layer), android/app/build.gradle (versionName and versionCode), and ios/YourApp/Info.plist (CFBundleShortVersionString and CFBundleVersion). Keeping these files in sync is a common source of friction—many teams automate this with tools like react-native-version or Fastlane lanes.

Build Number vs Version Number

While the version number is what users see, build numbers are strictly internal. On iOS, the build number (CFBundleVersion) must increment with every archive submitted to App Store Connect, even if the version string stays the same. On Android, versionCode must be a monotonically increasing integer. Automation that bumps build numbers on each CI run eliminates human error and rejected submissions.

Over-the-Air (OTA) Updates: Speed Without the App Store

React Native’s ability to deliver over-the-air (OTA) updates is one of its most powerful advantages. Because the bulk of your app logic is JavaScript (or TypeScript compiled to JS), you can push updates without requiring users to download a new binary from the store. OTA updates are ideal for quick bug fixes, layout tweaks, configuration changes, and minor feature flag toggles.

How OTA Updates Work

When your app launches, the OTA SDK (such as CodePush or EAS Update) checks against a remote server for a newer JS bundle or asset pack. If available, the new bundle is downloaded in the background and applied on the next cold restart or via a user-facing "Update Now" prompt. The critical constraint is that OTA updates cannot modify native code—only the JavaScript bundle and bundled assets (images, fonts, etc.). Changes to native modules, Gradle files, Podfiles, or the new architecture (Fabric, TurboModules) still require an App Store or Play Store submission.

CodePush (App Center) – The Battle-Tested Option

Microsoft’s CodePush, now part of App Center, remains a widely adopted solution. Deep integration requires installing react-native-code-push, linking the native library (autolinking with React Native 0.60+), and setting up deployment keys for staging and production environments. Releases are pushed via the CLI:

appcenter codepush release-react -a Owner/AppName -d Production

CodePush supports mandatory update flags (--mandatory), which force the app to apply the update before the user can continue, making it suitable for critical security fixes.

Expo Updates and EAS Update (Modern Alternative)

For teams using Expo or the Expo Development Build workflow, EAS Update is the recommended path. It integrates seamlessly with the Expo ecosystem, supports branching and channel-based deployments, and offers granular rollback capabilities. An update is published with:

eas update --branch production --message "Hotfix: login crash"

EAS Update also supports channel pinning, allowing you to target specific user segments (e.g., internal testers, beta group, production 10% rollout).

OTA Update Best Practices

  • Always test OTA updates on a staging channel before releasing to production. A broken JS bundle can render the app unusable for thousands of users.
  • Implement a rollback mechanism that the client can trigger remotely. For example, a kill switch feature flag that forces the app to load the last known good bundle.
  • Monitor bundle size. Large bundles lead to slow downloads and poor user experience. Use bundle analysis tools and consider lazy loading or code splitting for major features.
  • Handle update failures gracefully. Display a friendly message and offer a retry option, rather than crashing the app.

App Store and Play Store Updates: Version Control and Submission

While OTA updates cover the JS layer, all native changes—including SDK upgrades, new native modules, iOS/OS version target changes, and major UI overhauls—require a traditional app store submission. The submission process introduces a review latency that ranges from hours to several days, so you must plan your release cadence accordingly.

Versioning for Store Submissions

Update the version number in all required configuration files before building for store submission. For iOS, you edit Info.plist (or use Xcode’s project editor). For Android, you modify build.gradle. Using a centralized versioning tool like react-native-version or a Fastlane lane prevents mismatches:

npx react-native-version --never-amend

This updates app.json, Info.plist, and build.gradle in a single command, using the version specified in app.json.

Phased Rollouts and Staged Releases

Both Apple App Store Connect and Google Play Console support phased rollouts. For iOS, you can enable phased release within App Store Connect, which distributes the update over a 7-day period. For Android, you can use staged rollouts (5%, 10%, etc.) and monitor crash rates before expanding. This dramatically reduces the blast radius of a regression.

Forced Updates and Compatibility Checks

Not all users install updates immediately. Some will run older versions for weeks or months, which creates compatibility headaches if your backend API evolves. The industry standard solution is a forced update flow:

  1. On app launch (or after login), the client sends its current version to your API.
  2. The API responds with a minimum_version and latest_version.
  3. If current_version < minimum_version, show a blocking "Update Required" screen with a link to the store.
  4. If current_version < latest_version but above the minimum, show a non-blocking "New version available" prompt.

This approach keeps your user base on supported API versions and reduces support tickets related to "app not working."

Release Notes Best Practices

Write human-readable, benefit-oriented release notes for store listings. Avoid internal jargon. For example:

  • Instead of "Fixed race condition in useMemo causing stale closures in the checkout module," write "Improved payment stability and prevented rare checkout errors."
  • Include a call to action ("Update now for smoother shopping experience").

Automation: CI/CD for Version Bumping and Build Artifacts

Manual version management is error-prone and wastes developer time. Automating version increments, build number updates, and store uploads inside your CI/CD pipeline is one of the highest-ROI investments you can make.

Fastlane – The Swiss Army Knife

Fastlane provides lanes for incrementing build numbers, code signing, building, and uploading to TestFlight or Google Play. A typical lane for a React Native app:

lane :beta do
  increment_build_number(xcodeproj: "YourApp.xcodeproj")
  gradle(task: "clean assembleRelease")
  upload_to_testflight(skip_waiting_for_build_processing: true)
end

Fastlane also integrates with app versioning files via the dotenv plugin or by reading app.json directly.

Automated Build Numbers with CI Environment Variables

Many teams use the CI build number (e.g., GitHub Actions run number, CircleCI build number) as the Android versionCode and iOS CFBundleVersion. This guarantees uniqueness and eliminates "build number already used" errors from Apple. Example with a script:

export ANDROID_VERSION_CODE=$CIRCLE_BUILD_NUM
npx react-native-version --target android --never-amend

Artifact Management and Staged Distribution

Store build artifacts (APK, AAB, IPA) with proper naming conventions that include the version and build number. Distribute them to internal testers via services like TestFlight, Firebase App Distribution, or App Center. For EAS builds, Expo handles artifact management natively through the EAS servers.

Testing and Quality Assurance for Updates

Every update, whether OTA or a full binary release, carries risk. A structured QA process defends against regressions and user frustration.

Regression Testing Checklist for Updates

  • Core user flows (login, checkout, content rendering, push notifications).
  • Data migration and persistence (AsyncStorage, MMKV, SQLite) across versions.
  • Third-party SDK integrations (analytics, ads, auth providers).
  • Offline mode behavior (cache, queue, fallback).
  • Deep linking and universal links, which can break when navigation changes.

Beta and Canary Releases

Use TestFlight (iOS) and Internal Testing Track (Play Console) to distribute pre-release builds to a curated group of testers. For OTA updates, maintain a staging deployment channel that mirrors production. Once validated, promote the same bundle to production. Canary releases—where a small percentage of users get the update first—are possible with both EAS Update (channel pinning) and CodePush (deployment key segmentation with rollout percentage).

Monitoring and Crash Detection Post-Update

After releasing an update, monitor crash rates, error logs, and user feedback. Tools like Sentry, Firebase Crashlytics, and App Center Diagnostics provide real-time data segmented by app version. Set up alerts for a >1% crash rate increase immediately after a deployment. If a critical regression appears, activate a rollback plan.

Rollback Strategies: Containing Damage

Even with extensive testing, issues can slip into production. A well-defined rollback strategy protects your users and your reputation.

Feature Flags as a Shield

The most elegant rollback is a feature flag. If a new feature has a bug, disable it server-side without deploying any code. This works for OTA updates and binary releases alike. Implement a centralized flag service (LaunchDarkly, ConfigCat, or a custom endpoint) that your app checks at runtime. Feature flags complement updates by giving you a kill switch for defective functionality while keeping the rest of the release intact.

OTA Rollback

Both CodePush and EAS Update allow you to promote a previous bundle to the production deployment key. This reverts the JavaScript code to a known good state. For CodePush: appcenter codepush promote -a Owner/AppName -s Staging -d Production. EAS Update uses the dashboard or CLI to set a branch to a previous update. Keep in mind that the user’s device must launch again to download the rolled-back bundle—it is not instantaneous.

Binary Rollback

Rolling back a binary release is more painful because you must submit a new version to the store and wait for review. If your current version is critically broken, the best strategy is to (a) submit a hotfix incremented version (e.g., 2.1.1), (b) disable the broken feature via feature flags in the meantime, and (c) use forced update logic to push users to the hotfix. Never remove a version from the store that users already have installed, as that will not help them—only new installs will see the older version.

Server-Side Kill Switch

For severe issues where users must not access the app at all (e.g., a security vulnerability), implement a server-side kill switch. Your API or a dedicated endpoint returns a flag that forces the app to display a "Service Unavailable" or "Update Required" screen, effectively disabling functionality until the user updates. This is a nuclear option but may be necessary in emergencies.

Security Considerations for Updates

Updates are a vector for attacks if not handled securely.

  • Code signing and integrity checks. OTA platforms should sign the JS bundle, and the client should verify the signature before applying it. EAS Update uses code signing by default; CodePush supports optional signing via the App Center CLI. Enable these features to prevent man-in-the-middle or tampered-bundle attacks.
  • HTTPS for all update endpoints. Ensure that your update server and manifest URLs are served over HTTPS. App Transport Security (ATS) on iOS enforces this, but verify your Android network configuration as well.
  • Limit exposure of deployment keys. Never commit production deployment keys to version control. Use environment variables and secure secret storage in your CI provider.

Putting It All Together: A Production-Grade Update Workflow

A mature React Native team typically operates with the following workflow:

  1. Development – Feature branches, PRs, and code reviews.
  2. Staging – Automated CI builds (both binaries and OTA updates) are published to the staging environment. QA and internal testers validate.
  3. Binary Release – A version bump (minor or major) triggers App Store / Play Store submission. Phased rollout enabled.
  4. OTA Patches – Between binary releases, critical fixes are deployed as OTA updates to the stable production channel. Each OTA fix goes through the staging channel first.
  5. Monitoring – Crash dashboards and user feedback are continuously monitored. If a regression is detected, feature flags disable the broken feature, or an OTA rollback is executed.
  6. Forced Update – When a binary release includes a breaking API change or security fix, the minimum version is updated server-side, and all clients below that threshold see a blocking update screen.

This approach delivers speed of iteration without sacrificing reliability. Users benefit from rapid bug fixes and gradual feature rollouts, while the team maintains confidence in the release process.

External Resources

Conclusion

Handling app updates and versioning in React Native is not just about incrementing numbers—it is about designing a system that balances agility with stability. By combining semantic versioning, OTA updates for the JavaScript layer, phased binary releases for native changes, CI/CD automation, feature flags, and proactive monitoring, you can deliver a seamless experience to your users while maintaining full control over your deployment pipeline.

The key takeaway: invest in automation, testing, and observability upfront. Your future self—and your users—will thank you every time a hotfix goes out smoothly or a potentially catastrophic release is contained by a simple flag flip.