From react-native-plugin
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).
How this skill is triggered — by the user, by Claude, or both
Slash command
/react-native-plugin:rn-conventionsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
| Marker | Workflow |
| 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:
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
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 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.jsStatic (app.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):
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.
plugins: [
['expo-build-properties', { ios: { useFrameworks: 'static' }, android: { kotlinVersion: '1.9.0' } }],
['./plugins/withCustomManifest', { /* options */ }],
]
expo-constants:import Constants from 'expo-constants';
const apiUrl = Constants.expoConfig?.extra?.apiUrl;
import { Image } from 'react-native';
import { Image as ExpoImage } from 'expo-image';
// Static, bundled with app
<Image source={require('./assets/logo.png')} style={{ width: 100, height: 100 }} />
// Remote
<Image source={{ uri: 'https://example.com/photo.jpg' }} style={{ width: 100, height: 100 }} />
// Expo Image — better caching, faster decode, supports placeholders
<ExpoImage
source={require('./assets/logo.png')}
contentFit="cover"
transition={300}
placeholder={blurhash}
/>
For multiple resolutions: name files logo.png, [email protected], [email protected] — RN bundler picks based on screen density.
Expo:
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 <App />;
}
Pair with expo-splash-screen to prevent FOUC:
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.
import { Ionicons, MaterialIcons, Feather } from '@expo/vector-icons';
<Ionicons name="home" size={24} color="black" />
Bare projects: install react-native-vector-icons, link manually.
StyleSheet.create (default, fastest)const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
title: { fontSize: 18, fontWeight: '600' },
});
<View style={styles.container}>
<Text style={styles.title}>Hi</Text>
</View>
StyleSheet.create returns numeric IDs for style references — slightly more performant than inline objects on first render (deduplicates).
import { View, Text } from 'react-native';
<View className="flex-1 p-4">
<Text className="text-lg font-semibold">Hi</Text>
</View>
Install: pnpm add nativewind tailwindcss. Configure tailwind.config.js and babel.config.js.
import { Box, Text } from '@theme';
<Box flex={1} padding="m">
<Text variant="header">Hi</Text>
</Box>
Best for design-system-heavy apps.
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.
React Native uses Metro + Fast Refresh. Module-level mutable state breaks Fast Refresh — components don't re-render when the module changes.
// ❌ Module-level mutable
let counter = 0;
export function increment() { counter++; }
// ✅ 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.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.
ios/ or android/ files in Expo managed workflow. Use app.config.js config plugins.<ScrollView> for long dynamic lists (use FlatList or FlashList).<SafeAreaView> — content clipped by notch/home indicator.app.config.js env-driven values.<div>, <span>, <a>) — RN uses <View>, <Text>, <Pressable>/<Link>.process.env.X for runtime config in managed Expo — use Constants.expoConfig.extra or build-time replacement via babel.npx claudepluginhub aratkruglik/claude-sdlc --plugin react-native-pluginProvides production patterns for React Native apps with Expo: navigation, native modules, offline sync, cross-platform development. Use when architecting or building React Native projects.
Guides React Native and Expo development across UI, animations, state, testing, performance, and deployment. Activate when building or debugging mobile apps with these frameworks.
Guides React Native/Expo development with patterns for Expo Router navigation, state separation, data fetching (TanStack Query + Zod), styling, and native APIs.