<!-- skill: React Native Mobile | version: 1.0.0 | description: Build React Native apps with Expo, navigation and EAS releases | category: Mobile -->
## Instructions
- Expo vs bare: default to Expo managed (OTA + EAS Build) unless you need custom native modules — then `npx expo prebuild` (don't eject blindly); bare only for native SDK work.
- Setup: `npx create-expo-app myapp -t tabs`, file-based routing with `expo-router` (`app/(tabs)/index.tsx`), TypeScript `strict: true`, ESLint + Prettier from start.
- Navigation: expo-router stacks/tabs (`<Stack/>`, `<Tabs/>`, `useRouter().push("/detail/1")`, `useLocalSearchParams()`); deep links via `scheme` in app.json + `expo-linking`.
- State: React Query (`@tanstack/react-query`) for server state (cache/retry/pagination), Zustand or Context for client UI state; never `useState` waterfalls for fetched data.
- Storage: `expo-secure-store` for tokens, `AsyncStorage` for prefs/flags only; `expo-sqlite` for offline data; clear sensitive keys on logout.
- Networking: `fetch` or axios instance with baseURL + auth interceptor + `timeout`; zod validation on responses; offline banner with `@react-native-community/netinfo`.
- Native features: Expo SDK first (`expo-camera`, `expo-location`, `expo-notifications`, `expo-image-picker`); permissions via `useCameraPermissions()`-style hooks with rationale UI before system prompt.
- Styling: NativeWind or StyleSheet + theme tokens file; `44pt` touch targets, Dynamic Type (`allowFontScaling` default on), dark mode via `useColorScheme`.
- Errors: `ErrorBoundary` per tab + query `error` states with retry button; `expo-updates` fallback screen; log to Sentry (`@sentry/react-native`) with release + dist set.
- Testing: Jest + React Native Testing Library (`render`, `fireEvent`, `waitFor`), MSW for API mocks; Detox/Maestro for E2E on release builds before EAS submit.
- EAS Build: `eas.json` profiles (development/preview/production), `eas build --platform all --profile production`, credentials auto-managed; version via `app.json version + android.versionCode / ios.buildNumber`.
- EAS Submit/Update: `eas submit -p android/ios` to stores; `expo-updates` OTA for JS-only fixes (never for native changes — those need a new build).
## Examples
- Notes app: expo-router tabs (list + settings), React Query fetch, SecureStore token, SQLite offline cache, preview build on EAS.
- Camera share app: `expo-image-picker` + `expo-sharing`, upload via axios + zod, NetInfo offline queue, production EAS build + submit.
- OTA hotfix: JS text fix → `eas update --branch production` pushed instantly; native SDK bump → full `eas build` instead.
## Workflows
1. `create-expo-app` → router + theme + query client + storage helpers.
2. Screens per route + data hooks (React Query) + validation (zod).
3. Permissions + offline + error boundaries + dark mode pass.
4. `eas.json` profiles → development build → device testing (Expo Go only for dev).
5. Preview build → E2E + beta (TestFlight internal / Play internal).
6. Production build → `eas submit` → OTA channel for follow-up fixes.
## Templates
- Screen + data hook:
```tsx
// lib/api.ts
import { z } from "zod";
export const NoteSchema = z.object({
  id: z.string(), title: z.string().min(1), body: z.string()
});
export type Note = z.infer<typeof NoteSchema>;
export async function fetchNotes(token: string): Promise<Note[]> {
  const r = await fetch("https://api.example.com/v1/notes", {
    headers: { Authorization: `Bearer ${token}` },
  });
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return z.array(NoteSchema).parse(await r.json());
}
// app/(tabs)/index.tsx
import { useQuery } from "@tanstack/react-query";
import { Link } from "expo-router";
import { FlatList, Text, Button, View } from "react-native";
export default function Notes() {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ["notes"], queryFn: () => fetchNotes("TOKEN"),
  });
  if (isLoading) return <Text>Loading…</Text>;
  if (error) return (
    <View><Text>Error: {(error as Error).message}</Text>
    <Button title="Retry" onPress={() => refetch()} /></View>
  );
  return (
    <FlatList data={data} keyExtractor={(n) => n.id}
      refreshing={isLoading} onRefresh={() => refetch()}
      renderItem={({ item }) => (
        <Link href={`/detail/${item.id}`}><Text>{item.title}</Text></Link>
      )} />
  );
}
```
- eas.json:
```json
{
  "cli": { "version": ">= 7.0.0" },
  "build": {
    "development": { "developmentClient": true, "distribution": "internal" },
    "preview": { "distribution": "internal", "channel": "preview" },
    "production": { "channel": "production", "autoIncrement": true }
  },
  "submit": {
    "production": { "android": { "track": "internal" }, "ios": { "ascAppId": "1234567890" } }
  }
}
```
- app.json essentials:
```json
{
  "expo": {
    "name": "Notes", "slug": "notes", "scheme": "notes",
    "version": "1.2.0",
    "android": { "package": "com.example.notes", "versionCode": 34 },
    "ios": { "bundleIdentifier": "com.example.notes", "buildNumber": "34" }
  }
}
```
## Tools
- SARAH shell_run for `npx expo start/eas build/eas submit/eas update`, jest tests, lint.
- SARAH file_write for screens/hooks/api scaffolds; web_fetch for Expo/EAS docs.
## Advanced
- Perf: `FlashList` for long lists, `expo-image` with caching, `react-native-reanimated` on UI thread, Hermes enabled; Flipper + performance monitor for jank hunts.
- Push: `expo-notifications` token → backend, channels (Android) + permissions (iOS), deep-link on tap via `addNotificationResponseReceivedListener`.
- Bare path: `expo prebuild` to inspect native dirs, config plugins (`app.plugin.js`) instead of manual edits so `prebuild` stays repeatable.
## Documentation
- https://docs.expo.dev/
- https://docs.expo.dev/router/introduction/
- https://docs.expo.dev/eas/
- https://reactnative.dev/docs/getting-started
- https://tanstack.com/query/latest/docs/framework/react/overview
