skip to content

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?

level: seniorimportance: should knowfreq 32%

answer

  1. is the reported scheme wrong or stale?
  2. native config forcing light
  3. a leftover setColorScheme override
  4. snapshots at module scope or in state
  5. one subscriber feeding context

basics

~20 s

Either 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 s

First 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 lines
tsx
import { 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

for a junior

Recall that colours must come from useColorScheme or a context fed by it, not from a value read once.

for a middle

Explain the two fault classes, wrong reported scheme versus stale reads, and name the config and override causes of each.

for a senior

Run the diagnosis systematically, search the codebase for snapshots and forced overrides, and verify on development and release builds on both platforms.

for a principal

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.