--- name: sdlc-rn-conventions description: | React Native project structure, Expo vs bare workflow detection, app.json/app.config.js patterns, asset handling, fonts, styling approaches, hot-reload-friendly idioms. Use this skill to: - Detect Expo (managed/dev-client/EAS/ejected) vs bare RN workflow. - Pick correct project layout for each workflow. - Configure app.json / app.config.js with bundle ID, splash, icons, deep link schemes. - Handle assets, fonts, images correctly. - Pick a styling approach (StyleSheet / NativeWind / restyle / styled-components). Do NOT use this skill for: - Platform-specific branching (see rn-platform-specific). - Navigation (see rn-navigation). - Storage (see rn-state-and-storage). - Testing (see rn-testing). paths: ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"] --- # React Native Conventions ## Workflow detection | Marker | Workflow | |---|---| | `expo` in deps + `app.json` / `app.config.{js,ts}` + NO `ios/` `android/` | **Expo managed** | | `expo` + `expo-dev-client` in deps | **Expo dev-client** (custom native code via prebuild) | | `eas.json` present | **EAS Build** (managed or dev-client built in cloud) | | `expo` in deps + `ios/` + `android/` folders | **Expo ejected** (treat as bare for native code) | | No `expo`, `ios/` + `android/` folders | **Bare RN** (React Native CLI) | Detection precedence: ejected/bare overrides managed if native folders exist. For new RN projects in 2024+, Expo is the default. Bare projects are typically: - Legacy projects predating Expo SDK 49. - Projects with custom native modules that must be in source (rare with config plugins). - Apps with complex native integrations (CarPlay, Apple Watch, Wear OS). ## Project layouts ### Expo Router (modern, file-based) ``` project-root/ ├── app.json # OR app.config.{js,ts} for dynamic config ├── package.json ├── tsconfig.json ├── babel.config.js # plugins: ['babel-plugin-module-resolver', etc.] ├── metro.config.js # bundler config (optional) ├── eas.json # if using EAS Build ├── app/ # Expo Router root │ ├── _layout.tsx # root layout (always) │ ├── index.tsx # / (home) │ ├── +not-found.tsx # 404 │ ├── (auth)/ # group (parens hide from URL) │ │ ├── _layout.tsx # auth stack │ │ ├── login.tsx │ │ └── signup.tsx │ ├── (app)/ # authenticated app group │ │ ├── _layout.tsx # tabs/drawer │ │ ├── index.tsx # / │ │ ├── profile.tsx │ │ └── [id].tsx # dynamic │ └── _root.tsx # SafeAreaProvider, theme provider, etc. ├── components/ │ ├── ui/ # Button, Input, Card primitives │ └── features/ ├── hooks/ ├── lib/ ├── assets/ │ ├── icons/ │ ├── images/ │ └── fonts/ └── __tests__/ # OR colocated *.test.tsx ``` ### React Navigation (classical, modular routes) ``` project-root/ ├── app.json ├── package.json ├── tsconfig.json ├── App.tsx # entry: NavigationContainer + RootNavigator ├── src/ │ ├── navigation/ │ │ ├── RootNavigator.tsx # Auth/App switch │ │ ├── AuthNavigator.tsx │ │ ├── AppNavigator.tsx # tabs/drawer │ │ └── types.ts # ParamList types │ ├── screens/ │ │ ├── LoginScreen.tsx │ │ ├── HomeScreen.tsx │ │ └── ProfileScreen.tsx │ ├── components/ │ ├── hooks/ │ ├── lib/ │ └── assets/ └── __tests__/ ``` ### Bare workflow additions Bare RN projects also have: - `ios/` — Xcode project, Info.plist, Podfile. - `android/` — Gradle project, AndroidManifest.xml, build.gradle. - `index.js` (root entry registering the app via `AppRegistry.registerComponent`). For Expo managed, all native config flows through `app.json`/`app.config.js`. For bare, edit native files directly. ## `app.json` / `app.config.js` Static (`app.json`): ```json { "expo": { "name": "MyApp", "slug": "my-app", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#ffffff" }, "scheme": "myapp", "ios": { "bundleIdentifier": "com.example.myapp", "supportsTablet": true, "infoPlist": { "NSCameraUsageDescription": "Allow $(PRODUCT_NAME) to access your camera" } }, "android": { "package": "com.example.myapp", "permissions": ["CAMERA"], "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" } }, "plugins": [ "expo-router", ["expo-camera", { "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera" }] ], "extra": { "eas": { "projectId": "..." } } } } ``` Dynamic (`app.config.js`): ```js export default ({ config }) => ({ ...config, name: process.env.APP_VARIANT === 'dev' ? 'MyApp (Dev)' : 'MyApp', ios: { ...config.ios, bundleIdentifier: process.env.APP_VARIANT === 'dev' ? 'com.example.myapp.dev' : 'com.example.myapp', }, }); ``` Use dynamic config for env-aware bundle IDs (dev vs prod), feature flags, etc. ### Common app.config.js patterns - **Multiple variants** (dev/staging/prod) via env vars. - **Config plugins** to inject native changes without ejecting: ```js plugins: [ ['expo-build-properties', { ios: { useFrameworks: 'static' }, android: { kotlinVersion: '1.9.0' } }], ['./plugins/withCustomManifest', { /* options */ }], ] ``` - **Read from .env** via `expo-constants`: ```ts import Constants from 'expo-constants'; const apiUrl = Constants.expoConfig?.extra?.apiUrl; ``` ## Asset handling ### Images ```tsx import { Image } from 'react-native'; import { Image as ExpoImage } from 'expo-image'; // Static, bundled with app // Remote // Expo Image — better caching, faster decode, supports placeholders ``` For multiple resolutions: name files `logo.png`, `logo@2x.png`, `logo@3x.png` — RN bundler picks based on screen density. ### Fonts **Expo:** ```tsx import { useFonts } from 'expo-font'; export default function Layout() { const [loaded] = useFonts({ 'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'), 'Inter-Bold': require('./assets/fonts/Inter-Bold.ttf'), }); if (!loaded) return null; return ; } ``` Pair with `expo-splash-screen` to prevent FOUC: ```tsx import * as SplashScreen from 'expo-splash-screen'; SplashScreen.preventAutoHideAsync(); const [loaded] = useFonts({...}); useEffect(() => { if (loaded) SplashScreen.hideAsync(); }, [loaded]); ``` **Bare:** link via `react-native-asset` or manually in Xcode/Android Studio. ### Vector icons ```tsx import { Ionicons, MaterialIcons, Feather } from '@expo/vector-icons'; ``` Bare projects: install `react-native-vector-icons`, link manually. ## Styling approaches ### `StyleSheet.create` (default, fastest) ```tsx const styles = StyleSheet.create({ container: { flex: 1, padding: 16 }, title: { fontSize: 18, fontWeight: '600' }, }); Hi ``` `StyleSheet.create` returns numeric IDs for style references — slightly more performant than inline objects on first render (deduplicates). ### NativeWind (Tailwind for RN) ```tsx import { View, Text } from 'react-native'; Hi ``` Install: `pnpm add nativewind tailwindcss`. Configure `tailwind.config.js` and `babel.config.js`. ### restyle (Shopify, theme-driven) ```tsx import { Box, Text } from '@theme'; Hi ``` Best for design-system-heavy apps. ### styled-components/native ```tsx import styled from 'styled-components/native'; const Container = styled.View`flex: 1; padding: 16px;`; ``` Familiar to web devs but slightly slower than StyleSheet at scale. Pick what's installed. Don't introduce a new approach. ## Fast Refresh and module-level state React Native uses Metro + Fast Refresh. Module-level mutable state breaks Fast Refresh — components don't re-render when the module changes. ```ts // ❌ Module-level mutable let counter = 0; export function increment() { counter++; } ``` ```ts // ✅ Use Context, Zustand, or other React-aware state import { create } from 'zustand'; export const useCounter = create<{ count: number; inc: () => void }>((set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 })), })); ``` Hooks are Fast-Refresh-friendly by default. ## EAS Build (Expo managed deployment) `eas.json`: ```json { "cli": { "version": ">= 5.0.0" }, "build": { "development": { "developmentClient": true, "distribution": "internal" }, "preview": { "distribution": "internal" }, "production": {} }, "submit": { "production": {} } } ``` Run via `npx eas-cli build --profile production --platform all`. Cloud builds → `.ipa` (iOS) / `.aab` (Android) artifacts. Out of pipeline scope; documented for awareness. ## Anti-patterns - ❌ Editing `ios/` or `android/` files in Expo managed workflow. Use `app.config.js` config plugins. - ❌ Module-level mutable state — breaks Fast Refresh. - ❌ Storing JWTs in AsyncStorage. - ❌ Using `` for long dynamic lists (use `FlatList` or `FlashList`). - ❌ Forgetting `` — content clipped by notch/home indicator. - ❌ Hardcoding bundle IDs / API URLs — use `app.config.js` env-driven values. - ❌ Using web HTML primitives (`