--- name: expo-react-native description: Build and change a React Native app on Expo (managed workflow) with Expo Router and NativeWind. Use for any project on the "Expo — React Native" stack — screens, navigation, styling, state, lists, platform APIs. It is NOT a web project; the rules below are what keep that true. --- # React Native on Expo — how this project works This project renders with **React Native**, not the DOM. It previews in a browser through Expo web (react-native-web), but the code is native RN: the same files run on iOS and Android. Writing it like a web app produces something that renders nothing on device and usually nothing on web either. ## Primitives — MUST - Build UI from `react-native`: `View`, `Text`, `Pressable`, `ScrollView`, `Image`, `TextInput`, `FlatList`, `SectionList`, `Modal`, `Switch`, `ActivityIndicator`, `KeyboardAvoidingView`. - **NEVER** `div`, `span`, `p`, `button`, `a`, `img`, `ul`/`li`, `input`, `
`, or any HTML element. There is no `onClick` (`onPress`), no `href` (`` from `expo-router`), no `className`-less DOM. - **All text lives inside ``.** A bare string in a `` throws "Text strings must be rendered within a component". - No `window`, `document`, `localStorage`, `navigator`, `alert`, DOM events, `URL`/`fetch`-to-same-origin assumptions, or web-only npm packages (`react-router-dom`, `framer-motion`, `@mui/*`, anything importing `react-dom` directly). `fetch` itself is fine. ## Navigation — MUST - Routes are files under `app/`. `app/index.tsx` is `/`, `app/settings.tsx` is `/settings`, `app/item/[id].tsx` is a dynamic route. - Layouts are `app/_layout.tsx` (and nested `_layout.tsx`) exporting a ``, ``, or `` from `expo-router`. - Navigate with `` or `import { router } from "expo-router"; router.push("/item/42")`. Read params with `useLocalSearchParams()`. - Do not hand-roll a router, a history stack, or conditional screen rendering in one component. ## Styling — MUST - Prefer NativeWind: `className="flex-1 items-center gap-4 bg-white"`. It is wired (`global.css`, `metro.config.js`, `tailwind.config.js`, the babel preset). `StyleSheet.create({...})` is the alternative; both are fine, do not mix three systems. - No `.css` files beyond `global.css`. No cascading, no `:hover`, no descendant selectors, no `@media` — use `Pressable`'s state (`className="active:opacity-80"` or the `({ pressed }) =>` style prop) and `useWindowDimensions()` / `Platform` for conditionals. - Layout is **Flexbox only**, and `flexDirection` defaults to **`column`**, not `row`. `gap` works. There is no `display: grid`, no `float`, no `position: sticky`. - Respect notches: wrap screens in `SafeAreaView` from `react-native-safe-area-context` (the template already sets up the provider), or use `useSafeAreaInsets()`. ## Lists, images, input — MUST - Long or data-driven lists: `FlatList` / `SectionList` / `FlashList`, not `.map()` inside a `ScrollView`. Give `keyExtractor`. - Local images: ``. Remote images need an explicit `width`/`height` (or a sized parent) or they render 0×0. For anything real, prefer `expo-image`'s `` (better caching). - Text input is `` with `value` + `onChangeText`. There is no `` and no native submit — wire a `Pressable` that calls your handler. ## Platform APIs — reach for an Expo module `expo-image`, `expo-haptics`, `expo-linking`, `expo-constants`, `expo-clipboard`, `expo-file-system`, `expo-image-picker`, `expo-location`, `expo-notifications`, `expo-secure-store`, `expo-av`. Install with `npx expo install ` (not plain `npm install` — `expo install` picks the version that matches the SDK). **`expo-secure-store` is a no-op stub on web preview** — build against it, but do not tell the user their secret is stored securely until it runs on a device. Anything that needs a config plugin + native prebuild (most Bluetooth, some camera/ML modules) is out of scope for the web preview — say so rather than adding it. ## TypeScript gotchas — MUST - **Timers are numbers here, not `NodeJS.Timeout`.** There is no `@types/node` in an Expo app. Type a timer as `ReturnType` (or `number`), never `NodeJS.Timeout` / `NodeJS.Timer` — that fails `tsc --noEmit` with "Namespace 'NodeJS' has no exported member". - No `process.env` for app config — use `expo-constants` (`Constants.expoConfig?.extra`) or `EXPO_PUBLIC_*` vars read via `process.env.EXPO_PUBLIC_FOO` (the only `process.env` Metro inlines). - Event handler types come from `react-native` (`GestureResponderEvent`, `NativeSyntheticEvent<…>`, `LayoutChangeEvent`), not from `react`'s DOM event types (`MouseEvent`, `ChangeEvent`, `KeyboardEvent`). - `require()` for a local asset returns a number (an asset id), not a string. ## Scope — MUST - Build the screens and interaction that were asked for. Do **not** spend the turn reverse-engineering assets: decoding PNG frames pixel by pixel with inline `node -e` scripts, measuring sprite sheets, or profiling image data is almost never the task and it burns the whole step budget. If you need a frame count or a dimension, read it once, cheaply, and move on. If an asset genuinely can't be used without analysis you cannot do quickly, say so and ask — don't grind. ## Definition of done - `npm run typecheck` is clean (`tsc --noEmit`). - The Expo web preview renders the real screen on **every** route you added or changed — not a blank page, not a red Metro error overlay. Start the preview and look. - No HTML elements, no `document`/`window`, in any `.tsx` under `app/` or `components/`. - New screens are reachable through `expo-router` (a ``, a tab, or a `router.push`), not mounted conditionally inside another screen.