Skip to content
Expo Development Foundation
Esc
navigateopen⌘Jpreview
On this page

Settings screen

Route-only entry, sections, row component, typed settings writes.

All three apps ship the same screen shape: a route file that only re-exports, a screen component built from labeled sections, one row component handling every row kind (toggle, navigation, action), and every write going through the validated settings boundary from the data pattern.

When to use

  • App preferences: appearance, notifications, haptics, units, privacy flags.
  • Destructive or export actions that need a labeled, confirmable home (backup, export, clear data, sign out).

When not to use

  • One-off toggles embedded in a feature screen — a row component earns its place at three-plus rows.
  • Remote feature flags or server-driven config — those belong to the data boundary with caching, not to local settings state.

File map

src/app/(tabs)/settings.tsx   # route-only: <SettingsScreen /> (5 lines)
src/screens/settings.tsx       # sections composition, no raw controls
src/components/settings-row.tsx # SettingRow: icon, title, subtitle, right control
src/data/settings-store.tsx    # OR src/storage/settings.ts — typed getters/setters

Key excerpts (adapted)

Routes stay route-only — the screen is importable and testable on its own:

import { SettingsScreen } from '@/screens/settings';

export default function SettingsRoute() {
  return <SettingsScreen />;
}

One row component covers toggles, navigation, and actions:

type SettingRowProps = {
  title: string;
  subtitle?: string;
  onPress?: () => void;
  rightElement?: React.ReactNode; // Switch, chevron, value label
};

function SettingRow({ title, subtitle, onPress, rightElement }: SettingRowProps) {
  return (
    <Pressable
      onPress={onPress}
      disabled={!onPress}
      accessibilityRole={onPress ? 'button' : 'summary'}
      accessibilityLabel={title}
    >
      <Text>{title}</Text>
      {subtitle ? <Text>{subtitle}</Text> : null}
      {rightElement}
    </Pressable>
  );
}

Writes go through typed settings functions, never raw storage at the call site:

await setThemeMode('dark'); // validated + persisted behind the boundary

Live demo

Open the (patterns)/settings-screen route in the showcase/ app: Appearance, Notifications, and Data sections composed from one row component, with an export action and a confirmable destructive action. Expo Go is enough.

Anti-patterns

  • Business logic or data fetching inside the route file — routes compose, screens own behavior.
  • Raw storage writes from row handlers (AsyncStorage.setItem('theme', …)); every write passes the typed settings boundary.
  • Toggles without labels or state announcement — rows need accessibilityLabel and switch state surfaced to screen readers.
  • Destructive actions without confirmation and without an undo/export path.
  • Copy-pasting row markup per section instead of extending the one row component.

Provenance

Application labels are anonymized because the production source checkouts are private. They document observed SDK versions and file shapes, not publicly reproducible sources.

  • Production Expo 57 app C: route-only app/settings.tsx, screens/settings, components/settings-row.tsx, data/settings-store.tsx (typed settings + theme coupling).
  • Production Expo 57 app B: route-only app/(tabs)/settings.tsx, screens/settings.tsx, validated DEFAULT_SETTINGS domain defaults.
  • Production Expo 53 app A: (tabs)/settings.tsx sections (toggles via Switch, navigation chevrons, backup/export actions) over storage/settings.ts typed setters.

Was this page helpful?