A React Native banking app keeps light colors after the phone switches to dark mode with the app open; what are the likely causes and fixes?
answer
- is the reported scheme wrong or stale?
- native config forcing light
- a leftover setColorScheme override
- snapshots at module scope or in state
- one subscriber feeding context
basics
~20 sEither the reported scheme is wrong, from native config restricting the app to light or a leftover setColorScheme override, or it is right but not read reactively, from colours captured at module scope, in initial state or outside context.
solid answer
~40 sFirst log `useColorScheme()` in a component that renders when the switch happens. If it still says `'light'`, the platform is not letting the app go dark: an Expo `userInterfaceStyle` set to `light`, or absent, which defaults to light; on Android dev builds, `expo-system-ui` missing so that setting is ignored; a bare iOS `Info.plist` with `UIUserInterfaceStyle` set to light; or earlier code that called `Appearance.setColorScheme('light')` and never reset it. If the hook says `'dark'` but the card stays light, the scheme is read as a snapshot: `Appearance.getColorScheme()` in a module-scope `StyleSheet.create`, `useState(getColorScheme())`, or a hard-coded colour bypassing the palette. Fix by subscribing once with `useColorScheme()` in a provider, deriving colours at render, and correcting the config.
code
tsx · 12 linesimport { Appearance, StyleSheet } from 'react-native';
// Stale: evaluated once at import, never updated.
const isDark = Appearance.getColorScheme() === 'dark';
export const styles = StyleSheet.create({
card: {
padding: 20,
borderRadius: 12,
backgroundColor: isDark ? '#15181d' : '#ffffff',
},
});go deeper
Recall that colours must come from useColorScheme or a context fed by it, not from a value read once.
Explain the two fault classes, wrong reported scheme versus stale reads, and name the config and override causes of each.
Run the diagnosis systematically, search the codebase for snapshots and forced overrides, and verify on development and release builds on both platforms.
Set guardrails so theming regressions are caught early: lint rules against literal colours, a single appearance entry point, and dark-mode checks in release testing.
## Split the problem in two A screen that stays light after the system goes dark has one of two faults: 1. **The reported scheme is wrong**: React Native itself still reports `'light'`. 2. **The reported scheme is right but not used reactively**: React Native reports `'dark'`, yet some colours were computed once and never again. A one-line probe tells them apart: render `useColorScheme()` somewhere visible, or log it in a component that re-renders, then flip the system setting with the app open. ## When the reported scheme is wrong | Cause | Where it lives | Fix | |---|---|---| | Expo `userInterfaceStyle` is `light`, or missing, which defaults to light | `app.json` / app config | Set it to `automatic` | | Android development build without `expo-system-ui` | Project dependencies | Install it; otherwise the setting is ignored on Android | | Bare iOS app with `UIUserInterfaceStyle` set to Light | `Info.plist` | Use `Automatic` | | A forgotten `Appearance.setColorScheme('light')` | App code, perhaps a debug toggle | Reset with `'auto'` on 0.87 (`'unspecified'` on 0.86) | New Expo projects from the default template start with `automatic`, so this usually appears in older projects or after someone copied a config. The override case is the sneakiest: the app works in development until a settings screen, onboarding flow or test helper forces light and nothing ever resets it. ## When the scheme is right but colours are stale These are all the same mistake, reading a **snapshot** where a **subscription** is needed: - `const isDark = Appearance.getColorScheme() === 'dark'` at the top of a module, then colours baked into `StyleSheet.create`. Module code runs once, at import. - `const [scheme] = useState(Appearance.getColorScheme())` in a provider. The initial value never changes. - A provider that subscribes correctly, but a component that ignores it and uses a literal such as `'#ffffff'`. - A memoized component that reads the scheme through `getColorScheme()` rather than a hook or context, so nothing tells it to re-render. The fix pattern: 1. Call `useColorScheme()` in **one** provider near the root. 2. Choose a palette object from it and pass it through context. 3. Read colours with a hook in each component and apply them at render in style arrays. 4. Keep only layout in module-scope `StyleSheet.create`. ## A quick search for stale reads - Search for `getColorScheme(` outside event handlers and non-React utilities. - Search for hex literals in component files; each should come from the palette. - Search for `setColorScheme(` and confirm every forced value has a matching reset. - Check the app config and native project files for a hard-coded light style. ## An iOS-specific surprise The React Native docs note that when a screenshot is taken on iOS, the scheme may **flicker** between light and dark, because iOS snapshots the app in both schemes and appearance updates reach JavaScript asynchronously. For a banking app that re-fetches or logs on every scheme change, make scheme-change handling cheap and free of side effects: a colour swap, not a network call. ## Keeping it fixed A stuck-light bug usually comes back through a new component with a literal colour or a new debug toggle. Cheap guards: - A lint rule or code review check that flags hex literals in component files. - A single `applyAppearance()` entry point for every `setColorScheme` call. - A dark-mode pass on key screens in the release checklist. ## Verifying the fix - Flip the system setting with each key screen open: balance, transactions, transfer. - Test the scheduled switch if you can, since it happens while the app is backgrounded. - Relaunch the app in dark mode and confirm the first frame is already dark. - On an Android development build of an Expo app, confirm `expo-system-ui` is installed, since without it the `userInterfaceStyle` setting is ignored. - Use React Native DevTools' light and dark emulation (0.86 and later) to flip the scheme quickly while inspecting a screen, then confirm on the real system setting.
- Why can the color scheme briefly flicker when a screenshot is taken of a React Native app on iOS?iOS takes snapshots of the app in both light and dark appearances, and React Native delivers appearance updates to JavaScript asynchronously, so listeners can see a short light-dark-light sequence. The docs call this out for screenshots. Keep scheme-change handling to cheap colour updates so the flicker has no side effects.
- How would you prevent a debug appearance toggle from leaking into production?Route every `setColorScheme` call through one function that also records the choice, and have startup re-apply the stored preference, including `'auto'` for system. Then a forced value from a debug menu cannot silently survive, and a test can assert that the default path resets to following the system.
saying these in an interview costs you the question
- If useColorScheme says 'dark', the palette code must be right.
- A missing userInterfaceStyle in an Expo app defaults to automatic.
- Calling Appearance.getColorScheme() at module scope is fine because it is fast.
- Once the phone is dark, setColorScheme('light') calls from earlier stop applying.
- The scheme only changes on app restart, so stale colours are expected.