Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Appearance addChangeListener handler is called when app goes to background with wrong color scheme #28525

Closed
rosskhanas opened this issue Apr 4, 2020 · 48 comments
Labels
Resolution: Fixed A PR that fixes this issue has been merged.

Comments

@rosskhanas
Copy link

rosskhanas commented Apr 4, 2020

Description

A handler function of Appearance.addChangeListener is triggered when the app goes to the background. It also has a wrong colorScheme value.

React Native version:

System:
    OS: macOS 10.15.1
    CPU: (4) x64 Intel(R) Core(TM) i7-5557U CPU @ 3.10GHz
    Memory: 92.24 MB / 16.00 GB
    Shell: 5.7.1 - /bin/zsh
  Binaries:
    Node: 10.13.0 - /usr/local/bin/node
    Yarn: 1.12.1 - /usr/local/bin/yarn
    npm: 6.9.0 - /usr/local/bin/npm
    Watchman: Not Found
  Managers:
    CocoaPods: 1.8.4 - /usr/local/bin/pod
  SDKs:
    iOS SDK:
      Platforms: iOS 13.2, DriverKit 19.0, macOS 10.15, tvOS 13.2, watchOS 6.1
    Android SDK:
      API Levels: 23, 25, 26, 27, 28, 29
      Build Tools: 27.0.3, 28.0.2, 28.0.3, 29.0.2
      System Images: android-23 | Google APIs Intel x86 Atom, android-27 | Google Play Intel x86 Atom, android-28 | Google APIs Intel x86 Atom, android-28 | Google Play Intel x86 Atom, android-29 | Google APIs Intel x86 Atom
      Android NDK: Not Found
  IDEs:
    Android Studio: 3.5 AI-191.8026.42.35.5791312
    Xcode: 11.2.1/11B500 - /usr/bin/xcodebuild
  Languages:
    Python: 2.7.15 - /usr/local/bin/python
  npmPackages:
    @react-native-community/cli: Not Found
    react: 16.11.0 => 16.11.0 
    react-native: 0.62.1 => 0.62.1 
  npmGlobalPackages:
    *react-native*: Not Found

Steps To Reproduce

  1. Register Appearance.addChangeListener at the root of the app (I use it with react-native-navigation).
Appearance.addChangeListener(({ colorScheme }) => {
  console.log(colorScheme);
});
  1. Move the app to the background.

Expected Results

No theme changed.

@safaiyeh safaiyeh added Needs: Repro This issue could be improved with a clear list of steps to reproduce the issue. and removed Needs: Triage 🔍 labels Apr 8, 2020
@github-actions
Copy link

github-actions bot commented Apr 8, 2020

⚠️ Missing Reproducible Example
ℹ️ It looks like your issue is missing a reproducible example. Please provide a Snack or a repository that demonstrates the issue you are reporting in a minimal, complete, and reproducible manner.

@rosskhanas
Copy link
Author

Here is a repo: https://github.com/rosskhanas/react-native-appearance-bug

Once the app goes to the background it logs 2 rows - 2 different values to the console - light, and dark.

@github-actions github-actions bot added Needs: Attention Issues where the author has responded to feedback. and removed Needs: Author Feedback labels Apr 15, 2020
@adechassey
Copy link

adechassey commented Apr 23, 2020

+1, having the same issue!

@stale
Copy link

stale bot commented Jul 25, 2020

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as a "Discussion" or add it to the "Backlog" and I will leave it open. Thank you for your contributions.

@stale stale bot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Jul 25, 2020
@rosskhanas
Copy link
Author

Not stale

@stale stale bot removed the Stale There has been a lack of activity on this issue and it may be closed soon. label Jul 25, 2020
@manuhook
Copy link

manuhook commented Jul 30, 2020

The problem is on iOS only.
When backgrounding the app, perhaps due to a bug on iOS 13 the user interface style changes to the opposite color scheme and then back to the current color scheme immediately afterwards. The best solution is to debounce the notification calls by 10ms like they did on react-native-appearance.

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0,01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:RCTUserInterfaceStyleDidChangeNotification
                                                        object:self
                                                      userInfo:@{
                                                        RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey: self.traitCollection,
                                                      }];
  });

@david-cako
Copy link

Definitely still seeing this. Moved to expo/react-native-appearance for now.

@justincbeck
Copy link

justincbeck commented Aug 26, 2020

With this code in my App.js, I am seeing the extra calls but not the flash back and forth between themes:

  const systemScheme = useColorScheme();
  const themeObject = systemScheme === 'light' ? light : dark;

  const toggleTheme = useCallback((colorScheme) => {
    const statusBarTheme = colorScheme === 'light' ? 'dark' : 'light';
    StatusBar.setBarStyle(`${statusBarTheme}-content`);
  }, []);

  useEffect(() => {
    Appearance.addChangeListener(({ colorScheme }) => {
      toggleTheme(colorScheme);
    });
    return () => {
      Appearance.removeChangeListener();
    };
  }, [toggleTheme]);

Hope that helps someone.

@Macrow
Copy link

Macrow commented Sep 30, 2020

I have the same problem on iOS, and I found expo/react-native-appearance have this issue too, I don't know if it was my mistake.

I fix it by Lodash temporary, I don't find a better way.

import _ from 'lodash';

useEffect(() => {
    const handleColorModeChange = async (preferences: Appearance.AppearancePreferences) => {
      console.log(preferences.colorScheme);
      // do your job ....
    };

    // delay 1 second to handle change
    Appearance.addChangeListener(_.throttle(handleColorModeChange, 1000, {
      leading: false,
      trailing: true
    }));

    return () => {
      Appearance.removeChangeListener(handleColorModeChange);
    };
  }, []);

@jasonaibrahim
Copy link

jasonaibrahim commented Oct 29, 2020

@Macrow your workaround helped. thank you

here it is as a hook to use in lieu of the rn useColorScheme hook. I also used setTimeout instead of throttle

import { Appearance, ColorSchemeName } from 'react-native';
import { useEffect, useRef, useState } from 'react';

export default function useColorScheme(delay = 500): NonNullable<ColorSchemeName> {
  const [colorScheme, setColorScheme] = useState(Appearance.getColorScheme());

  let timeout = useRef<NodeJS.Timeout | null>(null).current;

  useEffect(() => {
    Appearance.addChangeListener(onColorSchemeChange);

    return () => {
      resetCurrentTimeout();
      Appearance.removeChangeListener(onColorSchemeChange);
    };
  }, []);

  function onColorSchemeChange(preferences: Appearance.AppearancePreferences) {
    resetCurrentTimeout();

    timeout = setTimeout(() => {
      setColorScheme(preferences.colorScheme);
    }, delay);
  }

  function resetCurrentTimeout() {
    if (timeout) {
      clearTimeout(timeout);
    }
  }

  return colorScheme as NonNullable<ColorSchemeName>;
}

@CalebLovell
Copy link

Thanks @jasonaibrahim, I really didn't want to fool with this nonsense today. That makes it easy! 🙌

@jgo80
Copy link

jgo80 commented May 16, 2021

Dump question, isn't it the easiest way to just call Appearance.getColorScheme() when the Listener fires? Works well for me without a cumbersome timer...

  const initAppearanceListener = () => {
    const listener: Appearance.AppearanceListener = ({ colorScheme } /* <-- ignore */) => {
      setColorScheme(Appearance.getColorScheme());
    };
    Appearance.addChangeListener(listener);
    return () => Appearance.removeChangeListener(listener);
  };

@Codelica
Copy link

@mrsimply you could, although I'm not sure what ignoring the colorScheme that's sent in really buys. Are you not seeing the listener fired twice with the wrong and then correct color scheme when the app is backgrounded?

I definitely do, so for now have just been ignoring the colorScheme listener calls when AppState says things are backgrounded.

@marcshilling
Copy link

Dump question, isn't it the easiest way to just call Appearance.getColorScheme() when the Listener fires? Works well for me without a cumbersome timer...

This worked for me, just ignoring the argument passed into the function

@michael2h4ng
Copy link

Here is a repo: https://github.com/rosskhanas/react-native-appearance-bug

Once the app goes to the background it logs 2 rows - 2 different values to the console - light, and dark.

I believe this is a feature on iOS 13 and above. It does this to take screenshots of the app for the App Switcher in case color scheme changes.

@outaTiME
Copy link

Dump question, isn't it the easiest way to just call Appearance.getColorScheme() when the Listener fires? Works well for me without a cumbersome timer...

  const initAppearanceListener = () => {
    const listener: Appearance.AppearanceListener = ({ colorScheme } /* <-- ignore */) => {
      setColorScheme(Appearance.getColorScheme());
    };
    Appearance.addChangeListener(listener);
    return () => Appearance.removeChangeListener(listener);
  };

Sorry, but as I say in the following link, it keeps happening very randomly with your workaround:

expo/expo#10815 (comment)

@zhumingcheng697
Copy link
Contributor

zhumingcheng697 commented Oct 4, 2022

@komik966 Very glad that you asked! I thought it would, and it almost did, but there were two minor cases where the DynamicColorIOS colors were not updated correctly in the app switcher, specifically:

  1. the custom border color I set for React Navigation header and tab bar
  2. the custom border color I set for NativeBase Input and Select components every single NativeBase component

Not sure if I suspect that it might be because borderColors are being handled differently, since every instance of borderColor is not updating correctly but all my other colors and backgroundColors are being handled perfectly fine in both React Navigation and NativeBase.

App.Switcher.MP4

At the end, I chose to use the same color for the borders under both light and dark mode to avoid the flashing when going to the app switcher.

@noumantahir
Copy link

https://reactnative.dev/docs/usecolorscheme This hook could help someone.

Experiencing flashing with useColorScheme hook as well..

"react-native": "0.70.4",

@JHPG
Copy link

JHPG commented Feb 21, 2023

Try to use Appearance.getColorScheme() instead.

https://reactnative.dev/docs/appearance

@s123121
Copy link

s123121 commented Apr 6, 2023

2 years passed and this bug still haunted me every other day

@efstathiosntonas
Copy link

efstathiosntonas commented Aug 2, 2023

this worked for me:

import { useEffect, useState } from "react";
import { Appearance, ColorSchemeName } from "react-native";
import debounce from "lodash/debounce";

// This hook is from https://github.com/facebook/react-native/issues/28525
export function useColorScheme(): NonNullable<ColorSchemeName> {
  const [colorScheme, setColorScheme] = useState(Appearance.getColorScheme());

  const initAppearanceListener = () => {
    const listener: Appearance.AppearanceListener = debounce(
      () => {
        setColorScheme(Appearance.getColorScheme());
      },
      200,
      { leading: false, trailing: true }
    );
    const changeListener = Appearance.addChangeListener(listener);
    return () => changeListener.remove();
  };

  useEffect(() => {
    initAppearanceListener();
  }, []);

  return colorScheme as NonNullable<ColorSchemeName>;
}

@imransilvake
Copy link

imransilvake commented Aug 11, 2023

I solved it finally using AppState 🎉.
Note: I am using NativeWind. Please adjust the code according to your needs.

import { useColorScheme as useNativeWindColorScheme } from 'nativewind';
import { AppState, useColorScheme } from 'react-native';

const Root = () => {
	const currentColorScheme = useColorScheme();
	const { colorScheme, setColorScheme } = useNativeWindColorScheme();
	const isDarkTheme = colorScheme === GlobalSchemeEnum.DARK;
	const LightTheme = { dark: !isDarkTheme, colors: ThemeNavigationLightColors };
	const DarkTheme = { dark: isDarkTheme, colors: ThemeNavigationDarkColors };
	const theme = isDarkTheme ? DarkTheme : LightTheme;

	useEffect(() => {
		const subscription = AppState.addEventListener('change', (state) => {
			const isActive = state === 'active';
			if (!isActive) return;
			currentColorScheme && setColorScheme(currentColorScheme);
		});
		return () => subscription.remove();
	}, [colorScheme, setColorScheme, currentColorScheme]);

	{/* Navigation */}
	<NavigationContainer theme={theme}>
		<App />		
	</NavigationContainer>
};

AppRegistry.registerComponent(appName, () => Root);

Let me know if it doesn't work!

zhxie added a commit to zhxie/conch-bay that referenced this issue Aug 13, 2023
zhxie added a commit to zhxie/conch-bay that referenced this issue Aug 23, 2023
@blueberry6401
Copy link

blueberry6401 commented Aug 25, 2023

2023, latest version, and this bug is still there.
My (workaround) solution is using lodash.debounce with leading = false.

@russeg
Copy link

russeg commented Aug 29, 2023

wow how is this bug still not fixed. i remember having this same bug in flutter, but there the issue was tagged, reproed, triaged, and fixed in less than 12 hours.

anyway, thanks for the many and varied workarounds.

@jrhager84
Copy link

It's kind of sad how many persistent bugs just sit for years without much notice. Oh well... :/

@bitcrumb
Copy link

Imho, you could argue if it is a bug or not, since the behavior follows the underlying iOS framework behavior. However, it does feel as unwanted behavior.

That being said, it is easily remediated by just checking if the change happens in the foreground. In our codebase we just query the current AppState in the listener and only if it is equal to "foreground" we act upon it.

@jrhager84
Copy link

Imho, you could argue if it is a bug or not, since the behavior follows the underlying iOS framework behavior. However, it does feel as unwanted behavior.

That being said, it is easily remediated by just checking if the change happens in the foreground. In our codebase we just query the current AppState in the listener and only if it is equal to "foreground" we act upon it.

But if someone changes their dark mode in the settings while the app is in background mode, it won't track.

At least the last time I tried it wouldn't work.

I would love to see your implementation if you wouldn't mind.

@komik966
Copy link

Hi all, did you try this solution #28525 (comment) ? This seems the best solution which also updates app screenshot in the app switcher.

@jrhager84
Copy link

Hi all, did you try this solution #28525 (comment) ? This seems the best solution which also updates app screenshot in the app switcher.

I'm using react navigation (which they indicated was incompatible)

@Codelica
Copy link

Codelica commented Aug 30, 2023

Maybe I'm missing something here, but I just ignore listener calls when the app is backgrounded and manually check the current system theme when the app becomes active. I guess that won't cover the screenshot change in the app switcher until they re-launch the app, but ¯(ツ)/¯

@vasylnahuliak
Copy link

vasylnahuliak commented Aug 30, 2023

My workaround (https://github.com/ds300/patch-package use for applying changes)

diff --git a/node_modules/react-native/React/CoreModules/RCTAppearance.mm b/node_modules/react-native/React/CoreModules/RCTAppearance.mm
index 17535a9..e7fd131 100644
--- a/node_modules/react-native/React/CoreModules/RCTAppearance.mm
+++ b/node_modules/react-native/React/CoreModules/RCTAppearance.mm
@@ -106,9 +106,8 @@ RCT_EXPORT_METHOD(setColorScheme : (NSString *)style)
 
 RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getColorScheme)
 {
-  if (_currentColorScheme == nil) {
-    _currentColorScheme = RCTColorSchemePreference(nil);
-  }
+  _currentColorScheme = RCTColorSchemePreference(nil);
+
   return _currentColorScheme;
 }

⚠️ PAY ATTENTION: These changes can create new bugs!

react-native+0.72.3.patch

@fabOnReact
Copy link
Contributor

fabOnReact commented Feb 2, 2024

how many devs experience this issue in the latest version of react-native?
The implementation of the method changed since this issue was opened.

@junebugfix
Copy link

This was fixed for me on 0.73.5! 🎉

@outaTiME
Copy link

outaTiME commented Feb 29, 2024

This was fixed for me on 0.73.5! 🎉

I'm using version 0.73.4 (and finally so far) it seems to be working properly too 🙌

@kelset kelset added Resolution: Fixed A PR that fixes this issue has been merged. and removed Needs: Repro This issue could be improved with a clear list of steps to reproduce the issue. Needs: Attention Issues where the author has responded to feedback. labels Feb 29, 2024
@kelset
Copy link
Contributor

kelset commented Feb 29, 2024

that's great! Given that there are a couple of reports of the issue being fixed let's close this. If you have any further issues I would recommend opening a new issue with a proper repro.

@kelset kelset closed this as completed Feb 29, 2024
@Andreibv
Copy link

Dump question, isn't it the easiest way to just call Appearance.getColorScheme() when the Listener fires? Works well for me without a cumbersome timer...

  const initAppearanceListener = () => {
    const listener: Appearance.AppearanceListener = ({ colorScheme } /* <-- ignore */) => {
      setColorScheme(Appearance.getColorScheme());
    };
    Appearance.addChangeListener(listener);
    return () => Appearance.removeChangeListener(listener);
  };

2024 not working for me :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Resolution: Fixed A PR that fixes this issue has been merged.
Projects
None yet
Development

No branches or pull requests