skip to content

In React Native, how does useColorScheme report the system light or dark setting, and why prefer it over Appearance.getColorScheme()?

level: juniorimportance: must knowfreq 68%

answer

  1. a hook versus a snapshot
  2. 'light', 'dark' or null
  3. re-renders on every scheme change
  4. sunset schedules and app overrides
  5. addChangeListener outside components

basics

~10 s

useColorScheme returns 'light' or 'dark' (null only without the native Appearance module) and re-renders the component whenever the scheme changes. Appearance.getColorScheme() returns the same value once, so a stored result goes stale.

solid answer

~40 s

`useColorScheme()` from `react-native` returns the **active** color scheme, `'light'` or `'dark'`, and `null` only when the native Appearance module is missing, for example on some out-of-tree platforms. It is built on `useSyncExternalStore` over `Appearance.addChangeListener`, so the component re-renders whenever the scheme changes: the user flips the system setting, a sunset schedule switches to dark, or the app calls `Appearance.setColorScheme()`. `Appearance.getColorScheme()` returns the same value but only at the moment you call it, so a value cached at module scope or in a ref goes stale. Use the hook in components, and `Appearance.addChangeListener`, which returns a subscription with `remove()`, in non-React code. In 0.87 the hook no longer returns `'unspecified'`; treat `null` as your default scheme.

code

tsx · 16 lines
tsx
import { StyleSheet, Text, View, useColorScheme } from 'react-native';

export function BalanceCard({ balance }: { balance: string }) {
  const scheme = useColorScheme();
  const dark = scheme === 'dark';
  return (
    <View style={[styles.card, { backgroundColor: dark ? '#1c1f24' : '#ffffff' }]}>
      <Text style={[styles.amount, { color: dark ? '#f2f4f7' : '#11151a' }]}>{balance}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  card: { padding: 20, borderRadius: 12 },
  amount: { fontSize: 28, fontWeight: '700' },
});

go deeper

for a junior

Recall that useColorScheme returns 'light' or 'dark' and re-renders on change, while getColorScheme returns a one-off value.

for a middle

Explain how the hook subscribes through addChangeListener and useSyncExternalStore, which events trigger it, and what null means.

for a senior

Structure theming so one subscription feeds the whole app, and remove stale module-scope or state-captured scheme reads from a codebase.

for a principal

Decide whether the product follows the system only or offers an in-app choice, and how that choice interacts with scheduled system switches.

## Two ways to read the scheme React Native exposes the user's appearance preference through the `Appearance` module, modelled on the web's `prefers-color-scheme`. On Android it maps to the system Dark theme (Android 10 and later), and on iOS to Dark Mode (iOS 13 and later). | API | Kind | Updates the UI when the scheme changes | |---|---|---| | `useColorScheme()` | React hook | Yes, re-renders the component | | `Appearance.getColorScheme()` | Function | No, returns the value at call time | | `Appearance.addChangeListener(listener)` | Subscription | Calls the listener; you decide what to update | The docs recommend the hook for components, and warn that styles or logic depending on the scheme should not cache `getColorScheme()`, because the value can change while the app runs. ## Return values - `'light'`: the light scheme is active. - `'dark'`: the dark scheme is active. - `null`: only when the native Appearance module is unavailable, which in practice means some out-of-tree platforms. Since React Native 0.87 the hook's return type is `ColorSchemeName | null` and it **no longer returns `'unspecified'`**. Code written for older versions that checked for `'unspecified'` can drop that branch; a simple `scheme === 'dark'` treats `null` as light. ## How the hook stays current The implementation is two lines of substance: 1. A `subscribe` function calls `Appearance.addChangeListener(onStoreChange)` and returns a cleanup that calls `remove()` on the subscription. 2. The hook returns `useSyncExternalStore(subscribe, getColorScheme)`. `useSyncExternalStore` re-reads `getColorScheme()` whenever the listener fires and re-renders only if the value changed. The listener fires for three kinds of change: - the user switches the system setting, - a scheduled switch, such as dark at sunset and light at sunrise, - the app itself calls `Appearance.setColorScheme('light' | 'dark' | 'auto')`. ## A banking app that follows the system For a banking app that simply follows the phone, the root of the theme is one hook call: ```tsx const scheme = useColorScheme(); const palette = scheme === 'dark' ? darkPalette : lightPalette; ``` Everything else, the balance card, the transaction list, the transfer form, reads its colours from that palette, usually through a context so that only one component subscribes. When the user's phone switches to dark at sunset, the hook fires once, the palette changes, and the screens re-render with the new colours. ## Outside React components Some code needs the scheme but is not a component, such as a chart library configured imperatively or a function that builds a native notification's colours: - Read the current value with `Appearance.getColorScheme()` at the moment you need it. - Subscribe with `const sub = Appearance.addChangeListener(({ colorScheme }) => { ... })`. - Call `sub.remove()` when you no longer need updates. On iOS and Android the `colorScheme` passed to the listener is always `'light'` or `'dark'`. ## Common mistakes - **Module-scope reads**: `const isDark = Appearance.getColorScheme() === 'dark'` at the top of a file is evaluated once, at import. - **Stuffing the value into state**: `useState(Appearance.getColorScheme())` captures the initial value and never updates. - **Assuming null means dark or means an error**: it only means the native module is absent. - **Subscribing in every component**: many components calling the hook works, but a single provider at the root is simpler and keeps the colour logic in one place. - **Forgetting the native configuration**: an app config or native project that restricts the app to the light style makes the reported scheme stay light even when the system is dark. ## Testing scheme changes without touching the device Since React Native 0.86, React Native DevTools can **emulate light and dark mode** from its command palette, so you can flip the scheme while stepping through a screen without opening the device's settings. The emulation is temporary and resets when DevTools disconnects. It complements, rather than replaces, a real test on devices: - Flip the real system setting with the app open to exercise the native path. - Relaunch in dark mode to check the first frame. - Leave the app running across a scheduled switch if the design depends on it.

  • Why does useColorScheme use useSyncExternalStore instead of useState plus an effect?
    The scheme is external state owned by the native Appearance module. `useSyncExternalStore` subscribes before the value is read for commit, so a change between render and subscription cannot be missed, and every component reading it during one render sees the same value. A `useState` initialised from `getColorScheme()` needs extra code to catch changes that happen before its effect subscribes.
  • What should a component do when useColorScheme returns null?
    Treat it as the app's default scheme, usually light. `null` means only that the native Appearance module is unavailable, which on iOS and Android does not happen in practice. A check such as `scheme === 'dark'` handles it naturally, with no separate branch.

saying these in an interview costs you the question

  • Appearance.getColorScheme() re-renders the component when the scheme changes.
  • useColorScheme returns null when the user has not chosen a scheme.
  • Reading the scheme once at app start is enough, since it only changes on restart.
  • useColorScheme still returns 'unspecified' in React Native 0.87.
  • The hook only reacts to the system setting, not to Appearance.setColorScheme calls.