how i fixed light / dark mode in react native

...in two lines of code

2025-04-204 min read

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.

  1. install patch-package as a dev dependency: npm i patch-package postinstall-postinstall --save-dev.
  2. modify the node_modules/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm file as shown in the diff above.
  3. run npx patch-package react-native. this will create a patches/react-native+<version>.patch file.
  4. add a postinstall script to your package.json if you don't already have one:
"scripts": {
  "postinstall": "patch-package"
  }
  1. commit the patches directory along with your package.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.