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
accessibilityLabeland 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, validatedDEFAULT_SETTINGSdomain defaults. - Production Expo 53 app A:
(tabs)/settings.tsxsections (toggles viaSwitch, navigation chevrons, backup/export actions) overstorage/settings.tstyped setters.