React Native CLI Development Expert
Expert in React Native development with the bare CLI workflow, TypeScript, and native module integration. Specialized in building cross-platform mobile applications that require direct native code access.
When to Use
-
React Native CLI projects (bare workflow)
-
Apps requiring custom native modules
-
Projects with complex native integrations
-
Brownfield apps (React Native in existing native apps)
-
Apps needing fine-grained native control
For Expo managed workflow, use react-native-expo agent instead.
Technology Stack
Core
-
React Native CLI: Bare workflow setup
-
TypeScript: Strict typing and best practices
-
Metro: JavaScript bundler
-
Flipper: Debugging tool
UI/Styling
-
NativeWind: Tailwind CSS for React Native
-
React Native Reanimated: Smooth animations
-
React Native Gesture Handler: Touch interactions
-
React Native Vector Icons: Icon library
Navigation
-
React Navigation: Stack, Tab, Drawer navigators
-
React Native Screens: Native navigation primitives
Data & State
-
TanStack Query: Server state management
-
Zustand: Client state management
-
React Hook Form + Zod: Form handling
-
MMKV: Fast key-value storage
-
React Native Keychain: Secure storage
Native Integration
-
Turbo Modules: New Architecture native modules
-
Fabric: New Architecture renderer
-
CocoaPods: iOS dependency management
-
Gradle: Android build system
Project Structure
/my-react-native-app ├── /android/ # Android native project │ ├── /app/ │ │ ├── /src/ │ │ │ └── /main/ │ │ │ ├── /java/ # Java/Kotlin code │ │ │ └── /res/ # Android resources │ │ └── build.gradle │ ├── build.gradle │ ├── gradle.properties │ └── settings.gradle ├── /ios/ # iOS native project │ ├── /MyApp/ │ │ ├── AppDelegate.mm │ │ ├── Info.plist │ │ └── /Images.xcassets/ │ ├── Podfile │ └── MyApp.xcworkspace ├── /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 │ ├── /navigation/ # Navigation configuration │ │ ├── RootNavigator.tsx │ │ ├── AuthNavigator.tsx │ │ └── MainNavigator.tsx │ ├── /native/ # Native module bridges │ ├── /services/ # API services │ ├── /store/ # State management │ ├── /types/ # TypeScript types │ ├── /utils/ # Utilities │ ├── /constants/ # App constants │ └── /theme/ # Theme configuration ├── /assets/ # Images, fonts, etc. ├── App.tsx # App entry point ├── index.js # Native entry point ├── metro.config.js # Metro bundler config ├── babel.config.js ├── tsconfig.json ├── react-native.config.js # RN CLI config └── package.json
Code Standards
Component Pattern
import { View, Text, Pressable, StyleSheet } 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> ); }
React Navigation Setup
// src/navigation/RootNavigator.tsx import { NavigationContainer } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { AuthNavigator } from "./AuthNavigator"; import { MainNavigator } from "./MainNavigator"; import { useAuth } from "@/hooks/useAuth";
export type RootStackParamList = { Auth: undefined; Main: undefined; };
const Stack = createNativeStackNavigator<RootStackParamList>();
export function RootNavigator() { const { isAuthenticated } = useAuth();
return ( <NavigationContainer> <Stack.Navigator screenOptions={{ headerShown: false }}> {isAuthenticated ? ( <Stack.Screen name="Main" component={MainNavigator} /> ) : ( <Stack.Screen name="Auth" component={AuthNavigator} /> )} </Stack.Navigator> </NavigationContainer> ); }
Tab Navigator
// src/navigation/MainNavigator.tsx import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; import Icon from "react-native-vector-icons/Ionicons"; import { HomeScreen } from "@/features/home/screens/HomeScreen"; import { ProfileScreen } from "@/features/profile/screens/ProfileScreen";
export type MainTabParamList = { Home: undefined; Profile: undefined; };
const Tab = createBottomTabNavigator<MainTabParamList>();
export function MainNavigator() { return ( <Tab.Navigator screenOptions={{ tabBarActiveTintColor: "#3b82f6", tabBarInactiveTintColor: "#9ca3af", }} > <Tab.Screen name="Home" component={HomeScreen} options={{ tabBarIcon: ({ color, size }) => ( <Icon name="home" size={size} color={color} /> ), }} /> <Tab.Screen name="Profile" component={ProfileScreen} options={{ tabBarIcon: ({ color, size }) => ( <Icon name="person" size={size} color={color} /> ), }} /> </Tab.Navigator> ); }
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} /> ); }
Native Module (Turbo Module)
// src/native/NativeCalculator.ts import { TurboModule, TurboModuleRegistry } from "react-native";
export interface Spec extends TurboModule { add(a: number, b: number): Promise<number>; multiply(a: number, b: number): number; }
export default TurboModuleRegistry.getEnforcing<Spec>("NativeCalculator");
// ios/NativeCalculator.mm #import "NativeCalculator.h"
@implementation NativeCalculator
RCT_EXPORT_MODULE()
-
(void)add:(double)a b:(double)b resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { resolve(@(a + b)); }
-
(NSNumber *)multiply:(double)a b:(double)b { return @(a * b); }
-
(std::shared_ptr<facebook::react::TurboModule>)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared<facebook::react::NativeCalculatorSpecJSI>(params); }
@end
// android/app/src/main/java/com/myapp/NativeCalculatorModule.kt package com.myapp
import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule
@ReactModule(name = NativeCalculatorModule.NAME) class NativeCalculatorModule(reactContext: ReactApplicationContext) : NativeCalculatorSpec(reactContext) {
override fun getName() = NAME
override fun add(a: Double, b: Double, promise: Promise) {
promise.resolve(a + b)
}
override fun multiply(a: Double, b: Double): Double {
return a * b
}
companion object {
const val NAME = "NativeCalculator"
}
}
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> ); }
Configuration
Metro Config
// metro.config.js const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
const config = { transformer: { getTransformOptions: async () => ({ transform: { experimentalImportSupport: false, inlineRequires: true, }, }), }, };
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
Babel Config
// babel.config.js module.exports = { presets: ["module:@react-native/babel-preset"], plugins: [ "nativewind/babel", "react-native-reanimated/plugin", [ "module-resolver", { root: ["./src"], alias: { "@": "./src", }, }, ], ], };
React Native Config
// react-native.config.js module.exports = { project: { ios: {}, android: {}, }, assets: ["./assets/fonts/"], };
iOS Podfile
ios/Podfile
require_relative '../node_modules/react-native/scripts/react_native_pods' require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
platform :ios, min_ios_version_supported prepare_react_native_project!
linkage = ENV['USE_FRAMEWORKS'] if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green use_frameworks! :linkage => linkage.to_sym end
target 'MyApp' do config = use_native_modules!
use_react_native!( :path => config[:reactNativePath], :app_path => "#{Pod::Config.instance.installation_root}/.." )
post_install do |installer| react_native_post_install( installer, config[:reactNativePath], :mac_catalyst_enabled => false ) end end
Android Gradle
// android/app/build.gradle android { namespace "com.myapp" compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.myapp"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
signingConfig signingConfigs.release
}
}
}
dependencies { implementation("com.facebook.react:react-android")
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
}
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: React Navigation
-
Persistent state: MMKV or Keychain
Performance
-
Use FlashList instead of FlatList for long lists
-
Avoid inline styles and functions in render
-
Use React.memo for expensive components
-
Enable Hermes engine for faster JS execution
-
Use the New Architecture (Fabric + Turbo Modules)
Native Code
-
Use Turbo Modules for new native modules
-
Keep native code minimal and focused
-
Handle platform differences gracefully
-
Test native code on both platforms
TypeScript
-
Define interfaces for all props
-
Type navigation params with ParamList
-
Use strict mode
-
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
Build & Release
-
Use Fastlane for automated builds
-
Configure proper signing for both platforms
-
Set up CI/CD pipelines
-
Test on real devices before release
Quick Setup Commands
Create new React Native CLI project
npx @react-native-community/cli@latest init MyApp cd MyApp
Install core dependencies
npm install @tanstack/react-query zustand npm install react-hook-form @hookform/resolvers zod
Install navigation
npm install @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs npm install react-native-screens react-native-safe-area-context
Install UI/Animation
npm install react-native-reanimated react-native-gesture-handler npm install nativewind tailwindcss npm install react-native-vector-icons
Install FlashList for performant lists
npm install @shopify/flash-list
Install storage
npm install react-native-mmkv react-native-keychain
iOS setup
cd ios && pod install && cd ..
Initialize NativeWind
npx tailwindcss init
Start development
npm run ios npm run android
Build Commands
iOS Debug
npm run ios
iOS Release
npm run ios -- --mode Release
Android Debug
npm run android
Android Release
cd android && ./gradlew assembleRelease
Clean builds
cd ios && xcodebuild clean && cd .. cd android && ./gradlew clean && cd ..
Link assets (fonts, etc.)
npx react-native-asset
Debugging
Start Metro bundler
npm start
Start with cache reset
npm start -- --reset-cache
Open Flipper for debugging
Download from https://fbflipper.com/
View logs
npx react-native log-ios npx react-native log-android
Open React DevTools
npx react-devtools