since starting work on my music tracking/sharing app, harmony (please check it out!!!!), i'm pretty sure i've
rewritten the app on three separate occasions. on every subsequent rewrite, i've ran into one persistent bug with the react-native
library that requires me to patch the library's native code.
the blinding lights
it's hard to notice if you aren't looking for it. basically, depending on what appearance the system is currently set to, the app will flash the opposite appearance for a split second before the correct appearance is applied.
for me, this is unacceptable. it's jarring and makes your app feel unpolished.
the weird thing i did notice, was that it only happened when jumping back into the app from the background. this told me it must have something to do with how react native handles backgrounding or initializing the app.
after digging through node_modules
(scary place, I know) and researching how appearance changes are handled, I found a little issue on github.
the cause
the relevant piece of native code is in RCTSurfaceHostingView.mm
. there's a method called traitCollectionDidChange:
. this method is part of UIKit and gets called whenever the environment traits change – this includes things like switching between light and dark mode, changing font sizes, etc.
// original snippet within traitCollectionDidChange:
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
// ... more code ...
// this notification triggers the appearance update in react native
[[NSNotificationCenter defaultCenter]
postNotificationName:RCTUserInterfaceStyleDidChangeNotification
object:self
userInfo:@{@"traitCollection": self.traitCollection}];
}
the problem is that this method can sometimes be called when the application isn't actually in the foreground (e.g., during startup sequences or potentially when backgrounded). when the RCTUserInterfaceStyleDidChangeNotification
is posted in these states, react native might try to update the UI based on an outdated trait collection. then, moments later, when the app is fully active and visible, it might receive another trait change or finalize its state, causing a second update to the correct appearance, resulting in the visible "flash".
the fix: two lines
the solution is surprisingly simple. we just need to ensure that we don't post this notification if the application is currently in the background. if the trait change happens while backgrounded, we can ignore it; the correct appearance will be picked up when the app becomes active anyway.
here's the patch:
--- a/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm
+++ b/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm
@@ -200,6 +200,11 @@ - (void)setActivityIndicatorViewFactory:(RCTSurfaceHostingViewActivityIndicatorV
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
+
+ // if the app is in the background, don't process the trait change here.
+ // it will be correctly handled when the app becomes active.
+ if (RCTSharedApplication().applicationState == UIApplicationStateBackground) {
+ return;
+ }
+
[[NSNotificationCenter defaultCenter]
postNotificationName:RCTUserInterfaceStyleDidChangeNotification
object:self
that's literally it.
we add a check for RCTSharedApplication().applicationState == UIApplicationStateBackground
. if the app is backgrounded, we simply return
before the notification is posted.
applying the patch
to make this fix persistent across installs, you should use patch-package
.
- install
patch-package
as a dev dependency:npm i patch-package postinstall-postinstall --save-dev
. - modify the
node_modules/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm
file as shown in the diff above. - run
npx patch-package react-native
. this will create apatches/react-native+<version>.patch
file. - add a
postinstall
script to yourpackage.json
if you don't already have one:
"scripts": {
"postinstall": "patch-package"
}
- commit the
patches
directory along with yourpackage.json
changes.
now, whenever you or someone else runs npm install
, patch-package
will automatically re-apply this fix.
conclusion
it's easy when you hit certain bugs deep into library code to just give up and say "oh the user wont notice / care enough". but sometimes, the fix is simpler than you think. these two lines of objective-c code completely eliminated the light/dark mode flash for me and made my app feel so much more polished.
hopefully, this post helps someone else facing the same blinding lights.