skip to content

Modals, Switches & Indicators

Modal, Switch, ActivityIndicator, StatusBar and Alert render real native controls with few styling hooks. Interviewers ask how Android's back button closes a modal and what StatusBar still controls.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

6

Why does a React Native Switch snap back after the user toggles it, and how do value and onValueChange fix it?

level: juniorimportance: must knowfreq 45%

answer

  1. native moves first
  2. JS value wins after render
  3. callback gets a boolean
  4. track colours keyed false/true
  5. revert on failed save

basics

~20 s

React Native's Switch is controlled: after the native toggle moves, the component resets it to the value prop. If onValueChange does not update the state behind value, the switch snaps back, so store the new boolean it receives.

solid answer

~40 s

`Switch` renders the native toggle, which animates as soon as the user taps. It then calls `onValueChange(newValue)` (and `onChange(event)`), and after the next render it compares the native state with your `value` prop. If they differ, it pushes your value back to the native control. So with `value={wifiOnly}` and a handler that only logs, the switch flips and returns. The fix is `onValueChange={setWifiOnly}`, or a handler that saves the setting and updates state. Style it with `trackColor={{false, true}}`, `thumbColor` and iOS-only `ios_backgroundColor`. The snap-back also lets JavaScript veto a change: an optimistic update that is reverted on a failed save shows the user that it did not stick.

code

tsx · 32 lines
tsx
import {useState} from 'react';
import {Switch, Text, View} from 'react-native';

type WifiOnlySettingProps = {
  initial: boolean;
  save: (wifiOnly: boolean) => Promise<void>;
};

export function WifiOnlySetting({initial, save}: WifiOnlySettingProps) {
  const [wifiOnly, setWifiOnly] = useState(initial);

  const onValueChange = async (next: boolean) => {
    setWifiOnly(next);
    try {
      await save(next);
    } catch {
      setWifiOnly(!next);
    }
  };

  return (
    <View style={{flexDirection: 'row', alignItems: 'center', gap: 12}}>
      <Text>Back up over Wi-Fi only</Text>
      <Switch
        value={wifiOnly}
        onValueChange={onValueChange}
        trackColor={{false: '#767577', true: '#34c759'}}
        ios_backgroundColor="#767577"
      />
    </View>
  );
}

go deeper

for a junior

Recall that Switch is controlled: pass value from state and update that state in onValueChange, which receives the new boolean.

for a middle

Explain why it snaps back: the native control moves first, and after the render Switch pushes the value prop back to native when the two differ.

for a senior

Use the controlled model on purpose, with optimistic updates reverted on failure or a disabled state while saving, and veto toggles that need a permission or confirmation first.

for a principal

Decide where settings live and how failures surface across the app, so every toggle follows one persistence and error-feedback pattern.

## A controlled native control React Native's `Switch` (from `react-native`) renders each platform's own native switch. It is **controlled**. The docs say it "requires an `onValueChange` callback that updates the `value` prop in order for the component to reflect user actions. If the `value` prop is not updated, the component will continue to render the supplied `value` prop." That explains the classic junior bug: a "Back up over Wi-Fi only" setting that flips when tapped and then **snaps back**. ## Why it snaps back The sequence inside `Switch` in React Native 0.87: 1. The user taps. The **native** switch animates to the new position straight away, because the OS control owns the gesture. 2. Native reports the change. `Switch` calls your `onChange(event)` and `onValueChange(newValue)`, then records the native value. 3. After the re-render, a layout effect compares the native value with your **`value` prop**. If they differ, it **pushes your value back to the native control**. 4. If your handler did not update state, `value` is still the old value, so the switch visibly returns. This is deliberate: it lets JavaScript veto a change. For example, you might refuse to enable a setting until a permission is granted. ## The props that matter | Prop | Meaning | |---|---| | `value` | the rendered state; treated as `false` when not `true` | | `onValueChange` | called with the **new boolean** | | `onChange` | called with the event; the value is in `nativeEvent.value` | | `disabled` | blocks toggling | | `trackColor` | `{false: color, true: color}` for the two track states | | `thumbColor` | the grip colour; on iOS setting it removes the grip's drop shadow | | `ios_backgroundColor` | iOS-only colour behind the track, visible when off or disabled | The iOS-only `onTintColor`, `thumbTintColor` and `tintColor` still exist but are deprecated in favour of `trackColor` and `thumbColor`. ## Saving the setting The switch usually mirrors a persisted setting. Two honest patterns: - **Optimistic**: set state to the new value at once, save in the background, and **revert state on failure**. The revert makes the switch snap back, which here is exactly the feedback you want. - **Pessimistic**: set `disabled` while saving and update `value` only after success. This is safer for settings with side effects, such as switching the backup network policy while an upload is running. Avoid reading the setting from storage on every render, or using the switch's own position as the source of truth. React state or a store is the source; `Switch` just reflects it. ## Disabling while a save is in flight For a setting with side effects, such as changing whether an upload already running may continue over mobile data, the pessimistic flow is worth writing out: 1. In `onValueChange`, store the requested value separately and set a `saving` flag. Do **not** change the state behind `value` yet. 2. Render the `Switch` with `disabled={saving}`, so a second tap cannot race the first. 3. The native switch has already moved. Because `value` has not changed, `Switch` moves it back, which briefly shows the old position while saving. If that flicker is unwanted, apply the requested value to `value` optimistically and revert on failure instead. 4. On success, set the state behind `value` to the requested value and clear `saving`. On failure, clear `saving` and explain what went wrong. Either way, the rule is the same: the switch shows your state, and your state changes only when you decide the change is real. ## Common mistakes - Treating `value` as an initial value, like an uncontrolled web checkbox. - Using `onChange` and reading `event.value` instead of `event.nativeEvent.value`, or simply not using `onValueChange`. - Styling the track with `backgroundColor` in `style` and wondering why iOS shows a different colour when off. Use `trackColor.false` and `ios_backgroundColor`. - Expecting identical visuals across platforms. The control is native, so size and shape follow each OS, and only the documented colour props are portable.

  • How can a React Native Switch refuse a toggle, for example until a permission is granted?
    Do not update the state behind `value` in `onValueChange`. The native switch moves, `Switch` sees that the native value no longer matches your `value` prop, and it pushes your value back. Then show why, for instance by prompting for the permission, and set the state only once it is granted.
  • Why does a React Native Switch look different on iOS and Android even with the same colours?
    It renders each platform's native switch, so shape, size and animation follow the OS. Only the documented colour props carry across: `trackColor` and `thumbColor`, plus `ios_backgroundColor` for the area behind the iOS track. On iOS, setting `thumbColor` also removes the grip's shadow.

saying these in an interview costs you the question

  • value only sets the Switch's initial state, like defaultValue.
  • onValueChange receives the change event, not a boolean.
  • Setting backgroundColor in style is how you colour the Switch track.
  • A Switch cannot refuse a toggle once the native control has moved.
  • Switch renders identically on iOS and Android.
open as a page

In React Native, what happens when the Android back button is pressed while a Modal is open, and why does onRequestClose matter?

level: middleimportance: must knowfreq 52%

basics

~20 s

Android's back press never closes a React Native Modal by itself: the modal's native dialog intercepts it and calls onRequestClose, whose handler must set visible to false. Without the handler, back does nothing, and BackHandler listeners do not fire while the modal is open.

open as a page

In React Native, how do you show a dimmed full-screen loading overlay with Modal and ActivityIndicator while a photo backup runs?

level: juniorimportance: should knowfreq 42%

basics

~20 s

Render a Modal with visible bound to backup state, transparent and animationType fade, fill it with a translucent full-size View, and centre an ActivityIndicator size large. transparent only removes the background, so the dim is your own View.

open as a page

In React Native, how do Alert.alert buttons for a confirm-delete prompt behave differently on iOS and Android?

level: middleimportance: should knowfreq 44%

basics

~20 s

Alert.alert shows a native dialog and returns nothing; each button's onPress delivers the choice. iOS allows any number of buttons with cancel and destructive styles. Android keeps the first three, maps them from the end to positive, negative and neutral, and ignores style.

open as a page

In React Native 0.87, what can the StatusBar component still control, and why were its backgroundColor and translucent props removed?

level: middleimportance: should knowfreq 30%

basics

~20 s

In React Native 0.87, StatusBar controls barStyle (icon colour), hidden, animated and the iOS showHideTransition. backgroundColor, translucent and networkActivityIndicatorVisible were removed because Android now draws apps edge to edge, so a status bar colour has no effect.

open as a page

In a React Native photo-backup app, why can a confirm-delete Modal flash blank or crash on iOS while closing, but not on Android, and how do you fix it?

level: seniorimportance: should knowfreq 18%

basics

~20 s

On iOS a React Native Modal keeps rendering its children until the native dismissal finishes and onDismiss fires; Android stops at once. Clearing the selected photo alongside visible={false} re-renders the closing iOS dialog with null. Clear it in onDismiss or render from a snapshot.

open as a page