react-native-expo
SKILL.md
React Native Development Expert
Expert in React Native development with Expo, TypeScript, and modern mobile tooling. Specialized in building performant cross-platform mobile applications with best practices.
When to Use
- React Native projects (Expo or bare workflow)
- Cross-platform mobile applications (iOS & Android)
- Mobile apps with native functionality
- Projects requiring native device features
For web-only React projects, use react agent instead.
Technology Stack
Core
- React Native: Cross-platform mobile framework
- Expo SDK 52+: Managed workflow and native APIs
- TypeScript: Strict typing and best practices
- Expo Router: File-based navigation
UI/Styling
- NativeWind: Tailwind CSS for React Native
- React Native Reanimated: Smooth animations
- React Native Gesture Handler: Touch interactions
- Expo Vector Icons: Icon library
Navigation
- Expo Router: File-based routing (recommended)
- React Navigation: Stack, Tab, Drawer navigators
Data & State
- TanStack Query: Server state management
- Zustand: Client state management
- React Hook Form + Zod: Form handling
- MMKV: Fast key-value storage
- Expo SecureStore: Secure data storage
Native APIs
- Expo Camera: Camera access
- Expo Notifications: Push notifications
- Expo Location: Geolocation
- Expo Image Picker: Media selection
- Expo FileSystem: File operations
Project Structure
/my-react-native-app
├── /app/ # Expo Router screens
│ ├── (tabs)/ # Tab navigator group
│ │ ├── index.tsx # Home tab
│ │ ├── profile.tsx # Profile tab
│ │ └── _layout.tsx # Tab layout
│ ├── (auth)/ # Auth screens group
│ │ ├── login.tsx
│ │ ├── register.tsx
│ │ └── _layout.tsx
│ ├── [id].tsx # Dynamic route
│ ├── _layout.tsx # Root layout
│ └── +not-found.tsx # 404 screen
├── /src/
│ ├── /components/ # Reusable components
│ │ ├── /ui/ # Base UI (Button, Input, Card)
│ │ ├── /forms/ # Form components
│ │ └── /lists/ # List components
│ ├── /features/ # Feature modules
│ │ ├── /auth/
│ │ │ ├── /components/
│ │ │ ├── /hooks/
│ │ │ ├── /services/
│ │ │ └── index.ts
│ │ └── /settings/
│ ├── /hooks/ # Custom hooks
│ ├── /services/ # API services
│ ├── /store/ # State management
│ ├── /types/ # TypeScript types
│ ├── /utils/ # Utilities
│ ├── /constants/ # App constants
│ └── /theme/ # Theme configuration
├── /assets/ # Images, fonts, etc.
├── app.json # Expo config
├── eas.json # EAS Build config
├── tailwind.config.js # NativeWind config
├── tsconfig.json
└── package.json
Code Standards
Component Pattern
import { View, Text, Pressable } from "react-native";
import { forwardRef } from "react";
import { cn } from "@/utils/cn";
interface ButtonProps {
variant?: "default" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
onPress?: () => void;
disabled?: boolean;
className?: string;
children: React.ReactNode;
}
const Button = forwardRef<View, ButtonProps>(
({ variant = "default", size = "md", className, children, ...props }, ref) => {
return (
<Pressable
ref={ref}
className={cn(
"items-center justify-center rounded-lg",
variants[variant],
sizes[size],
props.disabled && "opacity-50",
className
)}
{...props}
>
<Text className={cn("font-medium", textVariants[variant])}>
{children}
</Text>
</Pressable>
);
}
);
Button.displayName = "Button";
export { Button };
Custom Hook Pattern
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
export function useUsers() {
return useQuery({
queryKey: ["users"],
queryFn: () => userService.getAll(),
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: userService.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
}
Form Pattern (React Hook Form + Zod)
import { View, TextInput, Text, Pressable } from "react-native";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
type FormData = z.infer<typeof schema>;
export function LoginForm() {
const { control, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = (data: FormData) => {
// Handle submission
};
return (
<View className="gap-4">
<Controller
control={control}
name="email"
render={({ field: { onChange, onBlur, value } }) => (
<View>
<TextInput
className="border border-gray-300 rounded-lg px-4 py-3"
placeholder="Email"
onBlur={onBlur}
onChangeText={onChange}
value={value}
keyboardType="email-address"
autoCapitalize="none"
/>
{errors.email && (
<Text className="text-red-500 text-sm mt-1">
{errors.email.message}
</Text>
)}
</View>
)}
/>
<Controller
control={control}
name="password"
render={({ field: { onChange, onBlur, value } }) => (
<View>
<TextInput
className="border border-gray-300 rounded-lg px-4 py-3"
placeholder="Password"
onBlur={onBlur}
onChangeText={onChange}
value={value}
secureTextEntry
/>
{errors.password && (
<Text className="text-red-500 text-sm mt-1">
{errors.password.message}
</Text>
)}
</View>
)}
/>
<Pressable
className="bg-blue-500 rounded-lg py-3 items-center"
onPress={handleSubmit(onSubmit)}
>
<Text className="text-white font-semibold">Login</Text>
</Pressable>
</View>
);
}
Expo Router Layout
// app/_layout.tsx
import { Stack } from "expo-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import "../global.css";
const queryClient = new QueryClient();
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<QueryClientProvider client={queryClient}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
</Stack>
</QueryClientProvider>
</GestureHandlerRootView>
);
}
Tab Navigator Layout
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: "#3b82f6",
tabBarInactiveTintColor: "#9ca3af",
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: "Profile",
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}
List with FlashList
import { FlashList } from "@shopify/flash-list";
import { View, Text, Pressable } from "react-native";
interface User {
id: string;
name: string;
email: string;
}
interface UsersListProps {
users: User[];
onUserPress: (user: User) => void;
}
export function UsersList({ users, onUserPress }: UsersListProps) {
const renderItem = ({ item }: { item: User }) => (
<Pressable
className="bg-white p-4 border-b border-gray-100"
onPress={() => onUserPress(item)}
>
<Text className="font-semibold text-gray-900">{item.name}</Text>
<Text className="text-gray-500 text-sm">{item.email}</Text>
</Pressable>
);
return (
<FlashList
data={users}
renderItem={renderItem}
estimatedItemSize={72}
keyExtractor={(item) => item.id}
/>
);
}
Animation Pattern (Reanimated)
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
} from "react-native-reanimated";
import { Pressable } from "react-native";
export function AnimatedButton({ children, onPress }) {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const handlePressIn = () => {
scale.value = withSpring(0.95);
};
const handlePressOut = () => {
scale.value = withSpring(1);
};
return (
<Pressable
onPressIn={handlePressIn}
onPressOut={handlePressOut}
onPress={onPress}
>
<Animated.View style={animatedStyle}>{children}</Animated.View>
</Pressable>
);
}
App Configuration
// app.json
{
"expo": {
"name": "My App",
"slug": "my-app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"scheme": "myapp",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.company.myapp"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.company.myapp"
},
"plugins": [
"expo-router",
"expo-secure-store",
[
"expo-camera",
{
"cameraPermission": "Allow $(PRODUCT_NAME) to access your camera"
}
]
]
}
}
Best Practices
-
Component Organization
- Use feature-based folder structure
- Colocate related code (components, hooks, types)
- Use barrel exports (index.ts)
- Keep components small and focused
-
State Management
- Server state: TanStack Query
- Client state: Zustand
- Form state: React Hook Form
- Navigation state: Expo Router
- Persistent state: MMKV or SecureStore
-
Performance
- Use FlashList instead of FlatList for long lists
- Avoid inline styles and functions in render
- Use React.memo for expensive components
- Implement skeleton loaders for async content
- Use Reanimated for smooth animations
-
Styling
- Use NativeWind for Tailwind-like styling
- Support dark mode via useColorScheme
- Use consistent spacing and typography
- Handle safe areas with SafeAreaView
-
TypeScript
- Define interfaces for all props
- Use strict mode
- Type navigation params properly
- Use Zod for runtime validation
-
Platform Handling
- Use Platform.select() for platform-specific code
- Create .ios.tsx and .android.tsx files when needed
- Test on both platforms regularly
- Handle keyboard avoidance properly
-
Accessibility
- Add accessibilityLabel to interactive elements
- Use accessibilityRole appropriately
- Ensure adequate touch target sizes (44x44 minimum)
- Support dynamic text sizes
-
Error Handling
- Implement error boundaries
- Handle network errors gracefully
- Show meaningful error messages
- Add retry mechanisms for failed requests
Quick Setup Commands
# Create new Expo project
npx create-expo-app@latest my-app --template tabs
cd my-app
# Install core dependencies
npx expo install @tanstack/react-query
npm install zustand
npm install react-hook-form @hookform/resolvers zod
# Install UI/Animation
npx expo install react-native-reanimated react-native-gesture-handler
npm install nativewind tailwindcss
# Install FlashList for performant lists
npx expo install @shopify/flash-list
# Install storage
npx expo install react-native-mmkv expo-secure-store
# Initialize NativeWind
npx tailwindcss init
# Start development
npx expo start
EAS Build Configuration
// eas.json
{
"cli": {
"version": ">= 5.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {}
},
"submit": {
"production": {}
}
}
# Build for development
eas build --profile development --platform ios
# Build for production
eas build --profile production --platform all
# Submit to stores
eas submit --platform ios
eas submit --platform android
Weekly Installs
8
Repository
0xkynz/codekitGitHub Stars
1
First Seen
13 days ago
Security Audits
Installed on
opencode8
claude-code8
github-copilot8
codex8
amp8
cline8