Skip to content

Expo Fix: Improve New Architecture JS bundle reload on Android by prioritizing reactHost.reload() #20

Merged
CHOIMINSEOK merged 24 commits intoCodePushNext:masterfrom
naveen-bitrise:expo-fix
Jul 11, 2025
Merged

Expo Fix: Improve New Architecture JS bundle reload on Android by prioritizing reactHost.reload() #20
CHOIMINSEOK merged 24 commits intoCodePushNext:masterfrom
naveen-bitrise:expo-fix

Conversation

@naveen-bitrise
Copy link

Problem Description

This PR addresses an issue where CodePush updates fail to apply correctly on Android when using React Native's New Architecture, particularly in environments where the ReactHostDelegate implementation differs from what the library's reflection-based setJSBundle method assumes. (which is the case for Expo)

Currently, the CodePushNativeModule.java attempts to directly set a jsBundleLoader field on the ReactHostDelegate using Java reflection. This approach is brittle and can lead to exceptions (e.g., NoSuchFieldException) if the ReactHostDelegate implementation (such as ExpoReactHostDelegate in Expo-managed projects, or potentially other custom setups) does not have this specific internal field.

When this reflection fails, the exception can be caught by an outer error handler in the loadBundle method. This often prevents the more standard New Architecture reload mechanism, reactHost.reload(), from being called or from completing effectively, leading to the CodePush update not being applied without a full Activity restart.

Solution

This PR implements the following changes to CodePushNativeModule.java to improve the reliability of applying updates on Android with the New Architecture:

  1. Made setJSBundle(ReactHostDelegate, String) More Resilient:

    • The method that attempts to use reflection to set jsBundleLoader on ReactHostDelegate has been modified.
    • It now catches NoSuchFieldException (and potentially other reflection-related exceptions like IllegalAccessException or SecurityException) internally.
    • Instead of throwing a fatal error upwards that disrupts the update flow, it logs the issue (noting that this reflection path might not be available or necessary) and allows execution to continue. This makes the reflection attempt non-critical if it fails.
  2. Prioritized ReactHost.reload() in loadBundle() for New Architecture:

    • The New Architecture path within the loadBundle() method has been refactored to ensure that reactHost.reload("reason"); is consistently called.
    • The attempt to use the (now safer) reflection-based setJSBundle is made, but its failure no longer prevents reactHost.reload() from executing.
    • This prioritizes the standard React Native mechanism for reloading the JS bundle in the New Architecture, which is more robust and less dependent on internal implementation details of ReactHostDelegate.

Benefits

  • Enhanced Compatibility: Significantly improves CodePush compatibility with various React Native New Architecture setups on Android, especially those using custom ReactHostDelegate implementations (e.g., Expo).

How to Test (Example Scenario)

These changes were identified and tested in an environment using:

  • React Native (New Architecture enabled on Android)
  • Expo (bare workflow, post-prebuild)
  • @code-push-next/react-native-code-push version 10.0.1

In this environment, the previous implementation failed to apply updates due to NoSuchFieldException when interacting with ExpoReactHostDelegate. With these changes, CodePush updates are now applied correctly, with the JavaScript bundle reloading as expected.

@naveen-bitrise naveen-bitrise changed the title Expo-Fix: Improve New Architecture JS bundle reload on Android by prioritizing reactHost.reload() Expo Fix: Improve New Architecture JS bundle reload on Android by prioritizing reactHost.reload() May 19, 2025
Copy link

@CHOIMINSEOK CHOIMINSEOK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, @naveen-bitrise ! thank you for this contribution

While I read your pull request, I was wondering how expo-based React-Native app could be reloaded even the reflection is failed on ExpoReactHostDelegate. Can you explain more about this?

@CHOIMINSEOK
Copy link

CHOIMINSEOK commented Jun 1, 2025

Before we run regression tests across various environments, could you add regarding Demo App on Examples directory? (so that we will run regression tests on your setup) @naveen-bitrise

@naveen-bitrise
Copy link
Author

naveen-bitrise commented Jun 7, 2025

I have added an Example project CodePushExpoDemoApp in the Examples directory. For Expo there is a prebuild step npx expo prebuild --clean to create the ios and android folders. The Readme file in the folder has the instructions.

Please note that in package.json in the example, I could not use "@code-push-next/react-native-code-push": "file:../.." as it gave issues while creating the release bundle. I had to use "@code-push-next/react-native-code-push": "github:naveen-bitrise/react-native-code-push#expo-fix" instead. Before the PR is merged this line will need to be updated.

CodePushUtils.log("Could not get ReactHostDelegate from ReactHostImpl.");
}
} else {
CodePushUtils.log("ReactHost is not a direct ReactHostImpl instance (" + reactHost.getClass().getName() + "), skipping direct setJSBundle reflection attempt. This is expected with Expo.");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about trying to typecast to ExpoReactHost implementation & update jsbundleFile on it using reflection?
@naveen-bitrise

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception being caught is NoSuchFieldException here

Field 'jsBundleLoader' NOT FOUND on expo.modules.ExpoReactHostFactory$ExpoReactHostDelegate.

It might have a differently named field, or likely that Expo delegate is designed to not have its bundle loader set this way.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes They have a different implementation, so it must be failed. What I was telling you was we might be able to fix the reflection code working on ExpoReactHostDelegate

I think just replacing _jsBundleLoader backing field inside ExpoReactHostDelegate would be enough? ( It seems it's for dev mode, but i think we could use it for code push too)
https://github.com/expo/expo/blob/54d745bed821a8054a52237739a7b4571448bf89/packages/expo/android/src/rn78/main/expo/modules/ExpoReactHostFactory.kt#L39

Anyway, I'm running regression test for your PR and leave the result on it. It takes 2 hours or so.
@naveen-bitrise

@CHOIMINSEOK
Copy link

I'll do regression tests & leave a comment about the result here. In the mean time, I'll add github action script for test.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍👍
Note: Maybe we could use this as official code-push expo plugin ?
@kmsbernard @naveen-bitrise

@CHOIMINSEOK
Copy link

All regression tests are passed 🎉

New Architecture Android ✅
New Architecture iOS ✅
Old Architecture Android ✅
Old Architecture iOS ✅

@CHOIMINSEOK
Copy link

As current test code depends on bare react native app structure, we need to add new test code configuration for expo. Trying to setup this with ExpoDemoApp

@CHOIMINSEOK
Copy link

Note:
Since the Expo plugin will handle the native CodePush configuration, we don't need the native code template for Expo. Still we can continue using the current test template code. The native code can be overwritten using Expo’s native code generation, which will simplify the test app setup process.

To complete the Expo test setup, the following tasks remain:

  1. Configure the JavaScript side of the test app for the test environment (e.g., test server URL, test app name, test deployment key).
  2. Deploy the Expo plugin to make it easy to integrate.

@kmsbernard

Thanks for your patience 😅 Let’s aim to wrap this up by next Sunday!
@naveen-bitrise

@CHOIMINSEOK
Copy link

We added test case for expo codepush plugin, and it fails on 30 scenarios 🥲. We will start addressing this on next weekend
@kmsbernard @naveen-bitrise

@naveen-bitrise
Copy link
Author

@CHOIMINSEOK Is there anything I can do to help?

@CHOIMINSEOK
Copy link

@naveen-bitrise hey, I think we will address them this Sunday. Do you have time around 11:00 - 15:00 KST? Maybe we could share some failure cases

@naveen-bitrise
Copy link
Author

@CHOIMINSEOK that would be Saturday night time 10pm - 2 am for me as I am in US East Cost. I may not be available then, but I can review on Sunday morning based on the comments here.

@CHOIMINSEOK
Copy link

I see. I’ll leave comments about the remaining regression issues tomorrow. What time works best for you, by the way? I might be able to adjust my schedule to match yours. @naveen-bitrise

@CHOIMINSEOK
Copy link

We identified the issue — it turns out expo export generates a different bundle format compared to react-native bundle, which causes the CodePush client to fail at runtime when trying to locate the JS bundle. Given that the CodePush CLI is currently tightly coupled with the React Native CLI, this behavior is understandable.

Until we extend the CodePush CLI to support expo export, any Expo-based app will need to use react-native bundle (which is the default behavior of the CodePush CLI) to ensure compatibility.

That said, I’m curious — could this impact Expo app runtime? Are you currently using CodePush on top of an Expo app?

@naveen-bitrise

@naveen-bitrise
Copy link
Author

naveen-bitrise commented Jul 6, 2025

Yes, the CodePushExpoDemoApp app works with CodePush.

When you run npx expo prebuild, mentioned in the example's readme, it adds CodePush configuration needed, that's slightly different from a bare react native app for android.

The plugin file has to be in the CodePushExpoDemoApp folder. I will push a commit that moves the file back. Then you should be able to run the npx expo prebuild --clean command successfully.

@CHOIMINSEOK
Copy link

The plugin file has to be in the CodePushExpoDemoApp folder. I will push a commit that moves the file back.

It doesn't have to be. We can install the plugin via npm if we place the file in the root package. For easier usage, it have to be shipped in root. @naveen-bitrise

@naveen-bitrise
Copy link
Author

When you run npx expo prebuild, mentioned in the example's readme, it adds CodePush configuration needed, that's slightly different from a bare react native app for android.

This is how MainApplication.kt will look like.
https://gist.github.com/naveen-bitrise/43800a8bf190afaf009ee4b544dc97ed

…n app/build.gradle fixed

2. Added addtional debug looging to App.js
3. Update README
@naveen-bitrise
Copy link
Author

naveen-bitrise commented Jul 7, 2025

@CHOIMINSEOK I tested with CodePush to confirm that it works. Here are the steps that I did and the CodePush update was loaded successfully

  1. In Examples/CodePushExpoDemoApp, in app.json, the version was updated to 1.2.1 . (Please also update CodePushServerURL and CodePushDeploymentKey in app.json to the one that you use)
  2. From within Examples/CodePushExpoDemoApp folder, ran the command npx expo prebuild --clean
  3. From within Examples/CodePushExpoDemoApp/android folder ran ./gradlew assembleRelease. Installed the generated apk file Examples/CodePushExpoDemoApp/android/app/build/outputs/apk/release/app-release.apk in an android emulator
  4. Made a change in Examples/CodePushExpoDemoApp/app.js
  5. Generated an update bundle running the following command from Examples/CodePushExpoDemoApp folder
npx expo export:embed \
  --entry-file index.js \
  --platform android \
  --dev false \
  --reset-cache \
  --bundle-output ./build/index.android.bundle \
  --assets-dest ./build \
  --minify false
  1. zipped up the update bundle by running the following command from Examples/CodePushExpoDemoApp folder
zip -r update.zip ./build
  1. Uploaded update.zip to CodePush server
  2. In the Android Simulator killed and reopened the app. The update was detected and loaded. Please see screenshots below
Initial Update After

@naveen-bitrise
Copy link
Author

@CHOIMINSEOK I can connect during the week, this week during the day and anytime till 10 pm (EDT). I am away, from July 11 to 21.

@CHOIMINSEOK
Copy link

Thanks! I'll add a additional guide about how to use plugin tomorrow.
cc @kmsbernard

Copy link

@CHOIMINSEOK CHOIMINSEOK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really appreciate for your contribution and patience! @naveen-bitrise

@CHOIMINSEOK CHOIMINSEOK merged commit c020951 into CodePushNext:master Jul 11, 2025
vokhuyetOz pushed a commit to recodepush/react-native-code-push that referenced this pull request Oct 1, 2025
…oritizing reactHost.reload() (CodePushNext#20)

* classCastException caught in loadBundle

* removed debug comments

* Added Expo Example Project

* updated android bundle name to index.android.bundle

* Added iOS instructions to Readme in CodePushExpoDemoApp

* Updated iOS instructions in Readme in CodePushExpoDemoApp

* Updated Readme in CodePushExpoDemoApp to mention codepush expo plugin

* updated Readme to include zipping the updates

* minor Readme corrections

* add expo test-app setup-1

* refactored expo codepush plugin

* expo setup for test - 2

* expo setup for test - 3

* override TestAppName

* fix android test

* fix bundling script

* fix bundling script

* using react-native bundle instead of expo export during expo test

* moved expo plugin file to CodePushExpoDemoApp

* moved expo.js to root

* 1. Expo plugin refactor, and an issue in modifying buildTypes block in app/build.gradle fixed
2. Added addtional debug looging to App.js
3. Update README

* expo plugin refactor, and README update

* add expo plugin guide

---------

Co-authored-by: CHOIMINSEOK <cms3718@gmail.com>
Co-authored-by: Minsik Kim <kmsbernard@gmail.com>
Co-authored-by: Minseok Choi <minseokchoi@Minseoks-MacBook-Pro.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants