In react-native-restart 0.0.27, calling RNRestart.restart() on Android performed a true process restart via JakeWharton's ProcessPhoenix.triggerRebirth(...): the Android process was killed and relaunched, all native state was reinitialized.
In 0.0.28 (released as part of the RN 0.85 support PR, #287), the Android implementation was changed to call loadBundle() instead, which does instanceManager.recreateReactContextInBackground() — a soft reload equivalent to dev-mode "R, R". The JS bundle reruns, but the native process keeps running and native state (locale, native singletons, ContentProvider-initialised modules, etc.) is preserved.
This is a silent behavioural breaking change on Android. The API signature, package name and JS surface didn't change, so it is not flagged by typechecking or release notes, and iOS behaviour is unchanged. Any caller that depended on the hard-restart semantics (e.g. apply a runtime language change so native locale-dependent modules re-init) will silently stop working on Android while continuing to work on iOS.
The process-phoenix:2.2.0 dependency is still declared in android/build.gradle but is no longer referenced from the Java sources.
Diff that introduced the regression
android/src/main/java/com/reactnativerestart/RestartModule.java:
@ReactMethod
-public void Restart() {
- ProcessPhoenix.triggerRebirth(getReactApplicationContext());
+public void Restart(String reason) {
+ restartReason = reason;
+ loadBundle();
}
@ReactMethod
-public void restart() {
- ProcessPhoenix.triggerRebirth(getReactApplicationContext());
+public void restart(String reason) {
+ restartReason = reason;
+ loadBundle();
}
where loadBundle() is:
private void loadBundle() {
clearLifecycleEventListener();
try {
final ReactInstanceManager instanceManager = resolveInstanceManager();
if (instanceManager == null) return;
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override public void run() {
try {
instanceManager.recreateReactContextInBackground();
} catch (Throwable t) {
loadBundleLegacy(); // currentActivity.recreate()
}
}
});
} catch (Throwable t) {
loadBundleLegacy();
}
}
i.e. at worst it just calls Activity.recreate(), never a process kill.
Reproduction
- Any app on RN 0.80+ that uses
react-native-restart to apply a runtime change that requires native re-init (typical case: i18n language switch where the native side caches the locale at startup).
- Bump
react-native-restart from 0.0.27 → 0.0.28.
- Rebuild the Android app.
- Trigger
RNRestart.restart() from JS.
Expected (0.0.27): the Android process dies, the bootsplash flashes, the app re-launches cleanly, the new locale is picked up everywhere including native modules.
Actual (0.0.28): the JS bundle reloads in place, the bootsplash does not appear, the process PID is unchanged, and any native-side state from the previous run survives. To the user it looks like "the restart button did nothing meaningful".
Root cause
The PR titled "feat: react native 0.85 support" (avishayil/react-native-restart#287, commit 9312b24) replaced the Android implementation in order to unify it with the iOS soft-reload semantics. There is no mention in the changelog or README that the Android behaviour was being downgraded from a process restart to a context reload. The README still describes the package as "reload your app bundle during app runtime", which is technically accurate but glosses over the change.
The two behaviours are not equivalent:
|
0.0.27 Android |
0.0.28 Android |
iOS (unchanged) |
| Underlying call |
ProcessPhoenix.triggerRebirth |
recreateReactContextInBackground |
RCTTriggerReloadCommandListeners |
| Process killed |
yes |
no |
no |
Application.onCreate re-runs |
yes |
no |
n/a |
| Native singletons reset |
yes |
no |
no |
| ContentProvider-initialised modules re-init |
yes |
no |
n/a |
| Bootsplash shown again |
yes |
no |
depends |
For consumers who knowingly chose this library because it gave them the hard-restart behaviour, 0.0.28 is a regression.
Proposed fix
Either:
Option A — preserve the old Android semantics by default
Restore ProcessPhoenix.triggerRebirth(getReactApplicationContext()) as the Android implementation. The new getReason() API can still be supported by writing restartReason to SharedPreferences (or any persistent store) before the kill, and reading it back on next launch.
Option B — make it explicit and let the caller choose
Add an option to the JS API, e.g.:
RNRestart.restart({ reason?: string; hard?: boolean });
…where hard: true (the default, or at minimum supported) maps to ProcessPhoenix.triggerRebirth on Android. Document the difference between the two modes.
Either way, the changelog and README should explicitly call out that Android behaviour changed in 0.0.28.
Workaround for users who need the 0.0.27 behaviour today
Two options:
-
Pin to 0.0.27. Works on RN ≤ 0.84. Won't work on RN 0.85+ because of API changes the rest of 0.0.28 also brings in.
-
Yarn-patch 0.0.28 to swap loadBundle() back to ProcessPhoenix.triggerRebirth(...). The dependency on process-phoenix:2.2.0 is still declared in android/build.gradle, so no extra setup is needed. Minimal patch:
--- a/android/src/main/java/com/reactnativerestart/RestartModule.java
+++ b/android/src/main/java/com/reactnativerestart/RestartModule.java
@@ -100,13 +100,13 @@ public class RestartModule extends ReactContextBaseJavaModule {
@ReactMethod
public void Restart(String reason) {
restartReason = reason;
- loadBundle();
+ ProcessPhoenix.triggerRebirth(getReactApplicationContext());
}
@ReactMethod
public void restart(String reason) {
restartReason = reason;
- loadBundle();
+ ProcessPhoenix.triggerRebirth(getReactApplicationContext());
}
Caveat: because the process is killed, the static restartReason field is wiped, so RNRestart.getReason() will resolve to null on Android after a hard restart. If you need the reason to survive, persist it to disk before triggering the restart (or implement Option A above).
With Yarn Berry, this persists as a yarn patch; with npm, patch-package works.
In
react-native-restart0.0.27, callingRNRestart.restart()on Android performed a true process restart via JakeWharton'sProcessPhoenix.triggerRebirth(...): the Android process was killed and relaunched, all native state was reinitialized.In 0.0.28 (released as part of the RN 0.85 support PR, #287), the Android implementation was changed to call
loadBundle()instead, which doesinstanceManager.recreateReactContextInBackground()— a soft reload equivalent to dev-mode "R, R". The JS bundle reruns, but the native process keeps running and native state (locale, native singletons, ContentProvider-initialised modules, etc.) is preserved.This is a silent behavioural breaking change on Android. The API signature, package name and JS surface didn't change, so it is not flagged by typechecking or release notes, and iOS behaviour is unchanged. Any caller that depended on the hard-restart semantics (e.g. apply a runtime language change so native locale-dependent modules re-init) will silently stop working on Android while continuing to work on iOS.
The
process-phoenix:2.2.0dependency is still declared inandroid/build.gradlebut is no longer referenced from the Java sources.Diff that introduced the regression
android/src/main/java/com/reactnativerestart/RestartModule.java:where
loadBundle()is:i.e. at worst it just calls
Activity.recreate(), never a process kill.Reproduction
react-native-restartto apply a runtime change that requires native re-init (typical case: i18n language switch where the native side caches the locale at startup).react-native-restartfrom0.0.27→0.0.28.RNRestart.restart()from JS.Expected (0.0.27): the Android process dies, the bootsplash flashes, the app re-launches cleanly, the new locale is picked up everywhere including native modules.
Actual (0.0.28): the JS bundle reloads in place, the bootsplash does not appear, the process PID is unchanged, and any native-side state from the previous run survives. To the user it looks like "the restart button did nothing meaningful".
Root cause
The PR titled "feat: react native 0.85 support" (avishayil/react-native-restart#287, commit
9312b24) replaced the Android implementation in order to unify it with the iOS soft-reload semantics. There is no mention in the changelog or README that the Android behaviour was being downgraded from a process restart to a context reload. The README still describes the package as "reload your app bundle during app runtime", which is technically accurate but glosses over the change.The two behaviours are not equivalent:
ProcessPhoenix.triggerRebirthrecreateReactContextInBackgroundRCTTriggerReloadCommandListenersApplication.onCreatere-runsFor consumers who knowingly chose this library because it gave them the hard-restart behaviour, 0.0.28 is a regression.
Proposed fix
Either:
Option A — preserve the old Android semantics by default
Restore
ProcessPhoenix.triggerRebirth(getReactApplicationContext())as the Android implementation. The newgetReason()API can still be supported by writingrestartReasontoSharedPreferences(or any persistent store) before the kill, and reading it back on next launch.Option B — make it explicit and let the caller choose
Add an option to the JS API, e.g.:
…where
hard: true(the default, or at minimum supported) maps toProcessPhoenix.triggerRebirthon Android. Document the difference between the two modes.Either way, the changelog and README should explicitly call out that Android behaviour changed in 0.0.28.
Workaround for users who need the 0.0.27 behaviour today
Two options:
Pin to 0.0.27. Works on RN ≤ 0.84. Won't work on RN 0.85+ because of API changes the rest of 0.0.28 also brings in.
Yarn-patch 0.0.28 to swap
loadBundle()back toProcessPhoenix.triggerRebirth(...). The dependency onprocess-phoenix:2.2.0is still declared inandroid/build.gradle, so no extra setup is needed. Minimal patch:Caveat: because the process is killed, the static
restartReasonfield is wiped, soRNRestart.getReason()will resolve tonullon Android after a hard restart. If you need the reason to survive, persist it to disk before triggering the restart (or implement Option A above).With Yarn Berry, this persists as a
yarn patch; with npm,patch-packageworks.