mobile-framework-react-native
React Native mobile development patterns - New Architecture (Fabric, TurboModules, JSI), component architecture, React Navigation 7+, FlashList v2 optimization, gestures with Reanimated 4, platform-specific code, React 19 features
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-framework-react-native/skills/mobile-framework-react-native
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
React Native Development Patterns
Quick Guide: Build cross-platform mobile apps with React Native's New Architecture (default since 0.76). Use FlashList for performant lists (or FlatList with proper optimization). Use type-safe navigation hooks with static or dynamic API. Keep components small, memoize callbacks passed to lists, and test on both platforms from day one.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use FlashList (preferred) or FlatList for lists with more than 20 items - NEVER ScrollView with .map() for long lists)
(You MUST memoize renderItem callbacks and use stable keyExtractor functions - avoid key props on FlashList items as it breaks recycling)
(You MUST use react-native-safe-area-context for safe areas - React Native's built-in SafeAreaView is deprecated in 0.81+ and will be removed)
(You MUST test on BOTH iOS AND Android from day one - platform differences cause bugs)
(You MUST use Platform.select() or platform-specific files for platform differences - shadows, fonts, and feedback differ)
(You MUST be aware the New Architecture is enabled by default since React Native 0.76 - Fabric, TurboModules, and bridgeless mode)
</critical_requirements>
Auto-detection: React Native, react-native, React Navigation, @react-navigation, StyleSheet, FlatList, FlashList, ScrollView, View, Text, Pressable, TouchableOpacity, Platform.OS, Platform.select, SafeAreaView, KeyboardAvoidingView, Reanimated, Gesture Handler, TurboModules, Fabric, JSI, New Architecture
When to use:
- Building cross-platform iOS and Android mobile applications
- Creating native mobile UIs with React patterns
- Implementing mobile navigation with stack, tab, or drawer patterns
- Optimizing list performance with FlashList/FlatList and virtualization
- Adding gestures and animations with Reanimated 4
- Handling platform-specific code for iOS vs Android differences
- Working with React Native's New Architecture (Fabric, TurboModules, JSI)
Key patterns covered:
- New Architecture fundamentals (Fabric, TurboModules, JSI, bridgeless mode)
- Component architecture with accessibility props and platform-specific patterns
- React Navigation 7+ with type-safe hooks, static API, and auth flows
- FlashList/FlatList optimization with memoization and cell recycling
- Platform-specific code with Platform.select and file extensions
- Safe area and keyboard handling
- React 19 features (React Native 0.78+): useOptimistic,
use, ref as props
When NOT to use:
- Web-only React applications (use standard React patterns)
- React Native Web hybrid apps (requires additional considerations)
- Flutter, Swift, or Kotlin native development
Detailed Resources:
- examples/core.md - Component architecture, compound components
- examples/navigation.md - Type-safe navigation, auth flows, deep linking
- examples/styling.md - StyleSheet, design tokens, theming, responsive styling
- examples/performance.md - FlashList, FlatList optimization, memoization
- reference.md - Decision frameworks, checklists, CLI commands
<red_flags>
RED FLAGS
High Priority Issues:
- Using ScrollView + map() for lists with 50+ items - causes severe performance, use FlashList or FlatList
- Adding key prop to FlashList items - BREAKS cell recycling, eliminates FlashList's main benefit
- Using React Native's built-in SafeAreaView - deprecated in 0.81+, use react-native-safe-area-context
- Not testing on both platforms - iOS/Android differences compound; test daily on both
- Inline functions in FlatList/FlashList renderItem - creates new function every render, breaks memoization
- Using Reanimated 4 with old architecture - Reanimated 4.x is New Architecture ONLY
Medium Priority Issues:
- Hardcoded colors/spacing instead of constants - breaks consistency, makes theming impossible
- Not using Platform.select for shadows - iOS shadow props don't work on Android (use elevation)
- Missing keyboard handling on forms - keyboard covers inputs without KeyboardAvoidingView
- Using TouchableOpacity everywhere - Pressable is more flexible and supports android_ripple
Gotchas & Edge Cases:
- Android fontWeight only supports 'normal' and 'bold' reliably - 100-900 values may not work
- iOS shadow props are completely ignored on Android - must use elevation for Android shadows
- StatusBar backgroundColor only works on Android - iOS uses translucent status bar
- FlatList onEndReached fires immediately if data fits screen - use onEndReachedThreshold carefully
- KeyboardAvoidingView behavior differs: 'padding' for iOS, 'height' for Android
- React Native doesn't have CSS cascade - each component must have complete styles
- Text must be wrapped in
<Text>component - raw strings cause crashes - New Architecture enabled by default in 0.76+ - some older libraries may need updates
- FlashList v2 is New Architecture only - use v1 or FlatList if on old architecture
- React Native 0.78+ uses React 19 - propTypes removed, forwardRef optional
- React 19 adoption in recent SDK versions - check for breaking changes in your dependencies
- Android 15/16 enforces edge-to-edge - must handle safe areas properly
- Reanimated 4 requires react-native-worklets - Reanimated 3 will not work with it installed
- boxShadow and filter props are New Architecture only - not available on legacy architecture
- Reanimated 4: withSpring no longer uses restDisplacementThreshold/restSpeedThreshold - replaced by energyThreshold
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST use FlashList (preferred) or FlatList for lists with more than 20 items - NEVER ScrollView with .map() for long lists)
(You MUST memoize renderItem callbacks and use stable keyExtractor functions - avoid key props on FlashList items as it breaks recycling)
(You MUST use react-native-safe-area-context for safe areas - React Native's built-in SafeAreaView is deprecated in 0.81+ and will be removed)
(You MUST test on BOTH iOS AND Android from day one - platform differences cause bugs)
(You MUST use Platform.select() or platform-specific files for platform differences - shadows, fonts, and feedback differ)
(You MUST be aware the New Architecture is enabled by default since React Native 0.76 - Fabric, TurboModules, and bridgeless mode)
Failure to follow these rules will result in poor performance, platform-specific bugs, and broken UX on mobile devices.
</critical_reminders>
Files (skills)
-
examples
-
core.md 9.4 KB
# React Native - Core Patterns > Core component architecture and platform patterns. See [SKILL.md](../SKILL.md) for decision guidance. **Prerequisites**: Familiarity with React component patterns and TypeScript. --- ## Pattern 1: Component with Variants, Accessibility, and Loading ```typescript import { forwardRef, useCallback, useMemo, type ReactNode } from "react"; import { View, Text, Pressable, StyleSheet, ActivityIndicator, type ViewStyle, type TextStyle, } from "react-native"; // Design tokens as constants const COLORS = { primary: "#007AFF", secondary: "#5856D6", ghost: "transparent", text: "#FFFFFF", textGhost: "#007AFF", disabled: "rgba(0,0,0,0.3)", } as const; const SIZES = { sm: { paddingVertical: 8, paddingHorizontal: 12, fontSize: 14 }, md: { paddingVertical: 12, paddingHorizontal: 16, fontSize: 16 }, lg: { paddingVertical: 16, paddingHorizontal: 24, fontSize: 18 }, } as const; interface ButtonProps { children: ReactNode; variant?: "primary" | "secondary" | "ghost"; size?: "sm" | "md" | "lg"; disabled?: boolean; loading?: boolean; onPress: () => void; style?: ViewStyle; textStyle?: TextStyle; testID?: string; } export const Button = forwardRef<View, ButtonProps>( ( { children, variant = "primary", size = "md", disabled = false, loading = false, onPress, style, textStyle, testID, }, ref ) => { const isDisabled = disabled || loading; const buttonStyle = useMemo( () => [ styles.base, { backgroundColor: variant === "ghost" ? COLORS.ghost : COLORS[variant], paddingVertical: SIZES[size].paddingVertical, paddingHorizontal: SIZES[size].paddingHorizontal, }, variant === "ghost" && styles.ghostBorder, isDisabled && styles.disabled, style, ], [variant, size, isDisabled, style] ); const labelStyle = useMemo( () => [ styles.text, { fontSize: SIZES[size].fontSize }, variant === "ghost" && styles.ghostText, textStyle, ], [variant, size, textStyle] ); const handlePress = useCallback(() => { if (!isDisabled) { onPress(); } }, [isDisabled, onPress]); return ( <Pressable ref={ref} style={buttonStyle} onPress={handlePress} disabled={isDisabled} testID={testID} accessibilityRole="button" accessibilityState={{ disabled: isDisabled }} accessibilityLabel={typeof children === "string" ? children : undefined} > {loading ? ( <ActivityIndicator color={variant === "ghost" ? COLORS.primary : COLORS.text} /> ) : ( <Text style={labelStyle}>{children}</Text> )} </Pressable> ); } ); Button.displayName = "Button"; const styles = StyleSheet.create({ base: { borderRadius: 8, alignItems: "center", justifyContent: "center", flexDirection: "row", }, text: { color: COLORS.text, fontWeight: "600", }, ghostBorder: { borderWidth: 1, borderColor: COLORS.primary, }, ghostText: { color: COLORS.textGhost, }, disabled: { opacity: 0.5, }, }); ``` **Why good:** forwardRef for parent ref access, accessibilityRole/State for screen readers, useMemo prevents style object recreation, testID for E2E testing, named constants, loading state built-in --- ## Pattern 2: Compound Component with Reanimated ```typescript import { createContext, useContext, useState, useCallback, useMemo, type ReactNode, } from "react"; import { View, Text, Pressable, StyleSheet } from "react-native"; import Animated, { useAnimatedStyle, withTiming, } from "react-native-reanimated"; // Types interface AccordionContextValue { expandedId: string | null; toggle: (id: string) => void; } interface AccordionItemContextValue { id: string; isExpanded: boolean; } // Contexts const AccordionContext = createContext<AccordionContextValue | null>(null); const AccordionItemContext = createContext<AccordionItemContextValue | null>(null); // Hooks function useAccordion() { const context = useContext(AccordionContext); if (!context) { throw new Error("Accordion components must be used within Accordion.Root"); } return context; } function useAccordionItem() { const context = useContext(AccordionItemContext); if (!context) { throw new Error("AccordionItem components must be used within Accordion.Item"); } return context; } // Root Component function AccordionRoot({ children, defaultExpanded }: { children: ReactNode; defaultExpanded?: string }) { const [expandedId, setExpandedId] = useState<string | null>(defaultExpanded ?? null); const toggle = useCallback((id: string) => { setExpandedId((prev) => (prev === id ? null : id)); }, []); const value = useMemo(() => ({ expandedId, toggle }), [expandedId, toggle]); return ( <AccordionContext.Provider value={value}> <View style={styles.root}>{children}</View> </AccordionContext.Provider> ); } // Trigger Component with animated icon function AccordionTrigger({ children }: { children: ReactNode }) { const { toggle } = useAccordion(); const { id, isExpanded } = useAccordionItem(); const handlePress = useCallback(() => toggle(id), [toggle, id]); const iconStyle = useAnimatedStyle(() => ({ transform: [{ rotate: withTiming(isExpanded ? "180deg" : "0deg") }], })); return ( <Pressable onPress={handlePress} style={styles.trigger} accessibilityRole="button" accessibilityState={{ expanded: isExpanded }} > <Text style={styles.triggerText}>{children}</Text> <Animated.Text style={[styles.icon, iconStyle]}>▼</Animated.Text> </Pressable> ); } // Item and Content components follow same pattern function AccordionItem({ id, children }: { id: string; children: ReactNode }) { const { expandedId } = useAccordion(); const isExpanded = expandedId === id; const value = useMemo(() => ({ id, isExpanded }), [id, isExpanded]); return ( <AccordionItemContext.Provider value={value}> <View style={styles.item}>{children}</View> </AccordionItemContext.Provider> ); } function AccordionContent({ children }: { children: ReactNode }) { const { isExpanded } = useAccordionItem(); if (!isExpanded) return null; return <View style={styles.content}>{children}</View>; } // Export as compound component export const Accordion = { Root: AccordionRoot, Item: AccordionItem, Trigger: AccordionTrigger, Content: AccordionContent, }; const styles = StyleSheet.create({ root: { borderRadius: 8, overflow: "hidden", borderWidth: 1, borderColor: "#E5E5E5", }, item: { borderBottomWidth: 1, borderBottomColor: "#E5E5E5", }, trigger: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", padding: 16, backgroundColor: "#FAFAFA", }, triggerText: { fontSize: 16, fontWeight: "600", color: "#1A1A1A", }, icon: { fontSize: 12, color: "#666", }, content: { padding: 16, backgroundColor: "#FFFFFF", }, }); // Usage function FAQScreen() { return ( <Accordion.Root defaultExpanded="q1"> <Accordion.Item id="q1"> <Accordion.Trigger>What is React Native?</Accordion.Trigger> <Accordion.Content> <Text>React Native is a framework for building native mobile apps...</Text> </Accordion.Content> </Accordion.Item> <Accordion.Item id="q2"> <Accordion.Trigger>How does it work?</Accordion.Trigger> <Accordion.Content> <Text>React Native renders to native components...</Text> </Accordion.Content> </Accordion.Item> </Accordion.Root> ); } ``` **Why good:** Context-based compound component with Reanimated animations, accessibilityState tracks expanded state, throw on missing provider catches misuse early --- ## Pattern 3: Platform-Specific File Splitting When platform differences are significant (different haptic APIs, different UI feedback), use platform-specific file extensions. ``` components/ ├── button/ │ ├── button.tsx # Shared types/logic │ ├── button.ios.tsx # iOS-specific implementation │ ├── button.android.tsx # Android-specific implementation │ └── index.ts # Re-exports platform file ``` ```typescript // button.ios.tsx import { Pressable, Text } from "react-native"; import * as Haptics from "expo-haptics"; export function Button({ onPress, children }: ButtonProps) { const handlePress = () => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); onPress(); }; return ( <Pressable onPress={handlePress} style={styles.button}> <Text style={styles.text}>{children}</Text> </Pressable> ); } // button.android.tsx import { Pressable, Text } from "react-native"; import ReactNativeHapticFeedback from "react-native-haptic-feedback"; export function Button({ onPress, children }: ButtonProps) { const handlePress = () => { ReactNativeHapticFeedback.trigger("impactMedium"); onPress(); }; return ( <Pressable onPress={handlePress} android_ripple={{ color: "rgba(0,0,0,0.1)" }} style={styles.button} > <Text style={styles.text}>{children}</Text> </Pressable> ); } ``` **Why good:** Each platform uses native haptic API, Android gets ripple effect, Metro bundler auto-selects correct file based on platform -
navigation.md 14.2 KB
# React Native - Navigation Patterns > Type-safe navigation, authentication flows, and deep linking. See [core.md](core.md) for component patterns. **Prerequisites**: Familiarity with React Navigation concepts (stack, tab, drawer navigators). --- ## Type-Safe Navigation Setup ```typescript // navigation/types.ts import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; import type { BottomTabNavigationProp } from "@react-navigation/bottom-tabs"; import type { CompositeNavigationProp, RouteProp, } from "@react-navigation/native"; // Define param lists for each navigator export type RootStackParamList = { Auth: undefined; Main: undefined; Modal: { title: string }; }; export type AuthStackParamList = { Login: undefined; Register: undefined; ForgotPassword: { email?: string }; }; export type MainTabParamList = { Home: undefined; Search: { query?: string }; Profile: undefined; }; export type HomeStackParamList = { HomeScreen: undefined; ProductDetail: { productId: string }; CategoryList: { categoryId: string; categoryName: string }; }; // Composite navigation types for nested navigators export type HomeScreenNavigationProp = CompositeNavigationProp< NativeStackNavigationProp<HomeStackParamList, "HomeScreen">, CompositeNavigationProp< BottomTabNavigationProp<MainTabParamList>, NativeStackNavigationProp<RootStackParamList> > >; // Route types export type ProductDetailRouteProp = RouteProp< HomeStackParamList, "ProductDetail" >; ``` --- ## Type-Safe Navigation Hooks ```typescript // navigation/hooks.ts import { useNavigation, useRoute } from "@react-navigation/native"; import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; import type { RouteProp } from "@react-navigation/native"; import type { RootStackParamList, AuthStackParamList, HomeStackParamList, } from "./types"; // Typed navigation hooks export function useRootNavigation() { return useNavigation<NativeStackNavigationProp<RootStackParamList>>(); } export function useAuthNavigation() { return useNavigation<NativeStackNavigationProp<AuthStackParamList>>(); } export function useHomeNavigation() { return useNavigation<NativeStackNavigationProp<HomeStackParamList>>(); } // Typed route hook export function useTypedRoute< ParamList extends Record<string, object | undefined>, RouteName extends keyof ParamList >() { return useRoute<RouteProp<ParamList, RouteName>>(); } // Usage in component function ProductDetailScreen() { const navigation = useHomeNavigation(); const route = useTypedRoute<HomeStackParamList, "ProductDetail">(); const { productId } = route.params; const handleGoBack = () => { navigation.goBack(); }; const handleNavigateToCategory = (categoryId: string, categoryName: string) => { navigation.navigate("CategoryList", { categoryId, categoryName }); }; return ( <View> <Text>Product: {productId}</Text> </View> ); } ``` --- ## Authentication Flow Pattern ```typescript // navigation/root-navigator.tsx import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { useAuth } from "../hooks/use-auth"; import { AuthNavigator } from "./auth-navigator"; import { MainNavigator } from "./main-navigator"; import { SplashScreen } from "../screens/splash-screen"; import type { RootStackParamList } from "./types"; const Stack = createNativeStackNavigator<RootStackParamList>(); export function RootNavigator() { const { isAuthenticated, isLoading } = useAuth(); // Show splash while checking auth state if (isLoading) { return <SplashScreen />; } return ( <Stack.Navigator screenOptions={{ headerShown: false }}> {isAuthenticated ? ( <Stack.Screen name="Main" component={MainNavigator} /> ) : ( <Stack.Screen name="Auth" component={AuthNavigator} /> )} </Stack.Navigator> ); } // navigation/auth-navigator.tsx import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { LoginScreen } from "../screens/auth/login-screen"; import { RegisterScreen } from "../screens/auth/register-screen"; import { ForgotPasswordScreen } from "../screens/auth/forgot-password-screen"; import type { AuthStackParamList } from "./types"; const Stack = createNativeStackNavigator<AuthStackParamList>(); export function AuthNavigator() { return ( <Stack.Navigator screenOptions={{ headerShown: true, headerBackTitleVisible: false, }} > <Stack.Screen name="Login" component={LoginScreen} options={{ headerShown: false }} /> <Stack.Screen name="Register" component={RegisterScreen} options={{ title: "Create Account" }} /> <Stack.Screen name="ForgotPassword" component={ForgotPasswordScreen} options={{ title: "Reset Password" }} /> </Stack.Navigator> ); } ``` --- ## Tab Navigator with Nested Stacks ```typescript // navigation/main-navigator.tsx import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { HomeScreen } from "../screens/home/home-screen"; import { ProductDetailScreen } from "../screens/home/product-detail-screen"; import { SearchScreen } from "../screens/search/search-screen"; import { ProfileScreen } from "../screens/profile/profile-screen"; import type { MainTabParamList, HomeStackParamList } from "./types"; // Home Stack (nested in tab) const HomeStack = createNativeStackNavigator<HomeStackParamList>(); function HomeStackNavigator() { return ( <HomeStack.Navigator> <HomeStack.Screen name="HomeScreen" component={HomeScreen} options={{ headerShown: false }} /> <HomeStack.Screen name="ProductDetail" component={ProductDetailScreen} options={({ route }) => ({ title: "Product Details", })} /> </HomeStack.Navigator> ); } // Tab Navigator const Tab = createBottomTabNavigator<MainTabParamList>(); export function MainNavigator() { return ( <Tab.Navigator screenOptions={{ headerShown: false, tabBarActiveTintColor: "#007AFF", tabBarInactiveTintColor: "#8E8E93", }} > <Tab.Screen name="Home" component={HomeStackNavigator} options={{ tabBarLabel: "Home", tabBarIcon: ({ color, size }) => ( <TabIcon name="home" color={color} size={size} /> ), }} /> <Tab.Screen name="Search" component={SearchScreen} options={{ tabBarLabel: "Search", tabBarIcon: ({ color, size }) => ( <TabIcon name="search" color={color} size={size} /> ), }} /> <Tab.Screen name="Profile" component={ProfileScreen} options={{ tabBarLabel: "Profile", tabBarIcon: ({ color, size }) => ( <TabIcon name="person" color={color} size={size} /> ), }} /> </Tab.Navigator> ); } ``` --- ## Modal Navigation Pattern ```typescript // navigation/root-navigator.tsx - with modals import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { ModalScreen } from "../screens/modal-screen"; const Stack = createNativeStackNavigator<RootStackParamList>(); export function RootNavigator() { const { isAuthenticated } = useAuth(); return ( <Stack.Navigator> <Stack.Group screenOptions={{ headerShown: false }}> {isAuthenticated ? ( <Stack.Screen name="Main" component={MainNavigator} /> ) : ( <Stack.Screen name="Auth" component={AuthNavigator} /> )} </Stack.Group> {/* Modal screens accessible from anywhere */} <Stack.Group screenOptions={{ presentation: "modal", headerShown: true, }} > <Stack.Screen name="Modal" component={ModalScreen} options={({ route }) => ({ title: route.params.title, })} /> </Stack.Group> </Stack.Navigator> ); } // Opening modal from anywhere function SomeScreen() { const navigation = useRootNavigation(); const openModal = () => { navigation.navigate("Modal", { title: "My Modal" }); }; return ( <Button onPress={openModal}>Open Modal</Button> ); } ``` --- ## useFocusEffect for Resource Management ```typescript import { useCallback } from "react"; import { useFocusEffect } from "@react-navigation/native"; // WebSocket connection management function ChatScreen({ roomId }: { roomId: string }) { useFocusEffect( useCallback(() => { // Setup: Connect when screen is focused const ws = new WebSocket(`wss://chat.example.com/rooms/${roomId}`); ws.onopen = () => { console.log("Connected to chat"); }; ws.onmessage = (event) => { // Handle incoming messages }; // Cleanup: Disconnect when screen loses focus return () => { ws.close(); }; }, [roomId]) ); return <ChatUI />; } // Analytics tracking function ProductScreen({ productId }: { productId: string }) { useFocusEffect( useCallback(() => { // Track screen view when focused analytics.trackScreenView("ProductScreen", { productId }); const startTime = Date.now(); // Track time spent when leaving return () => { const timeSpent = Date.now() - startTime; analytics.trackTimeSpent("ProductScreen", { productId, timeSpent }); }; }, [productId]) ); return <ProductDetails productId={productId} />; } // Polling data while focused function NotificationsScreen() { const [notifications, setNotifications] = useState([]); useFocusEffect( useCallback(() => { // Start polling when focused const fetchNotifications = async () => { const data = await api.getNotifications(); setNotifications(data); }; fetchNotifications(); const interval = setInterval(fetchNotifications, 30000); // Stop polling when unfocused return () => { clearInterval(interval); }; }, []) ); return <NotificationList notifications={notifications} />; } ``` --- ## Screen Preloading Pattern ```typescript import { useNavigation } from "@react-navigation/native"; import { useCallback } from "react"; function ProductList({ products }: { products: Product[] }) { const navigation = useNavigation(); // Preload product detail screen when user hovers/long-presses const handlePreload = useCallback( (productId: string) => { // React Navigation 7+ supports preloading if ("preload" in navigation) { (navigation as any).preload("ProductDetail", { productId }); } }, [navigation] ); const renderItem = useCallback( ({ item }: { item: Product }) => ( <ProductCard product={item} onPress={() => navigation.navigate("ProductDetail", { productId: item.id }) } onLongPress={() => handlePreload(item.id)} /> ), [navigation, handlePreload] ); return ( <FlatList data={products} renderItem={renderItem} keyExtractor={(item) => item.id} /> ); } ``` --- ## Deep Linking Configuration ```typescript // navigation/linking.ts import type { LinkingOptions } from "@react-navigation/native"; import type { RootStackParamList } from "./types"; export const linking: LinkingOptions<RootStackParamList> = { prefixes: ["myapp://", "https://myapp.com"], config: { screens: { Auth: { screens: { Login: "login", Register: "register", ForgotPassword: "forgot-password", }, }, Main: { screens: { Home: { screens: { HomeScreen: "", ProductDetail: "product/:productId", CategoryList: "category/:categoryId", }, }, Search: "search", Profile: "profile", }, }, Modal: "modal/:title", }, }, // Custom URL parsing getStateFromPath: (path, config) => { // Handle custom URL formats if (path.startsWith("/p/")) { const productId = path.replace("/p/", ""); return { routes: [ { name: "Main", state: { routes: [ { name: "Home", state: { routes: [ { name: "HomeScreen" }, { name: "ProductDetail", params: { productId } }, ], }, }, ], }, }, ], }; } // Default behavior return undefined; }, }; // App.tsx import { NavigationContainer } from "@react-navigation/native"; import { linking } from "./navigation/linking"; function App() { return ( <NavigationContainer linking={linking}> <RootNavigator /> </NavigationContainer> ); } ``` --- ## Navigation State Persistence ```typescript // Persist navigation state across app restarts import AsyncStorage from "@react-native-async-storage/async-storage"; import { NavigationContainer, type NavigationState } from "@react-navigation/native"; import { useCallback, useEffect, useState } from "react"; const NAVIGATION_STATE_KEY = "NAVIGATION_STATE"; function App() { const [isReady, setIsReady] = useState(false); const [initialState, setInitialState] = useState<NavigationState | undefined>(); useEffect(() => { const restoreState = async () => { try { const savedStateString = await AsyncStorage.getItem(NAVIGATION_STATE_KEY); const state = savedStateString ? JSON.parse(savedStateString) : undefined; setInitialState(state); } finally { setIsReady(true); } }; if (!isReady) { restoreState(); } }, [isReady]); const onStateChange = useCallback((state: NavigationState | undefined) => { if (state) { AsyncStorage.setItem(NAVIGATION_STATE_KEY, JSON.stringify(state)); } }, []); if (!isReady) { return <SplashScreen />; } return ( <NavigationContainer initialState={initialState} onStateChange={onStateChange} > <RootNavigator /> </NavigationContainer> ); } ``` -
performance.md 15.5 KB
# React Native - Performance Patterns > FlashList/FlatList optimization, memoization, lazy loading, and profiling. See [core.md](core.md) for component patterns. **Prerequisites**: Understand list virtualization concepts and React.memo basics. --- ## Pattern 1: FlashList (Recommended for New Architecture) FlashList v2 provides superior performance through cell recycling instead of virtualization. **FlashList v2 is New Architecture only.** Key improvements: no more estimatedItemSize required, up to 50% reduced blank area, built-in masonry layout support, and automatic item resizing. ```typescript import { FlashList } from "@shopify/flash-list"; import { memo, useCallback } from "react"; import { View, Text, Pressable, StyleSheet } from "react-native"; // Constants const ITEM_HEIGHT = 80; interface Product { id: string; name: string; price: number; category: string; } interface ProductItemProps { item: Product; onPress: (id: string) => void; } // Memoized item component - CRITICAL: Do NOT add key prop (breaks recycling) const ProductItem = memo(function ProductItem({ item, onPress }: ProductItemProps) { const handlePress = useCallback(() => { onPress(item.id); }, [item.id, onPress]); return ( <Pressable onPress={handlePress} style={styles.item}> <View style={styles.details}> <Text style={styles.name} numberOfLines={1}> {item.name} </Text> <Text style={styles.price}>${item.price.toFixed(2)}</Text> </View> </Pressable> ); }); // Main list component with FlashList interface ProductListProps { products: Product[]; onProductPress: (id: string) => void; onEndReached?: () => void; } export function ProductListFlash({ products, onProductPress, onEndReached, }: ProductListProps) { // Stable renderItem with useCallback const renderItem = useCallback( ({ item }: { item: Product }) => ( <ProductItem item={item} onPress={onProductPress} /> ), [onProductPress] ); // Use getItemType for different item types (improves recycling) const getItemType = useCallback((item: Product) => { return item.category; // Items of same category share recycling pool }, []); return ( <FlashList data={products} renderItem={renderItem} // FlashList v2: estimatedItemSize is OPTIONAL (auto-calculates from actual measurements) // FlashList v1: estimatedItemSize is REQUIRED for performance // Providing it in v2 can still help with initial render estimatedItemSize={ITEM_HEIGHT} // Use getItemType for heterogeneous lists getItemType={getItemType} // Performance optimizations onEndReached={onEndReached} onEndReachedThreshold={0.5} showsVerticalScrollIndicator={false} /> ); } const styles = StyleSheet.create({ item: { height: ITEM_HEIGHT, flexDirection: "row", alignItems: "center", paddingHorizontal: 16, backgroundColor: "#FFFFFF", }, details: { flex: 1, }, name: { fontSize: 16, fontWeight: "600", color: "#1A1A1A", }, price: { fontSize: 14, color: "#007AFF", marginTop: 4, }, }); ``` **Why FlashList v2 is better:** - Cell recycling instead of virtualization (reuses component instances) - Up to 50% less blank area while scrolling (v2 on New Architecture) - Maintains 60 FPS even with complex items - Automatic item sizing in v2 (no estimatedItemSize required - measures real items) - Built-in masonry layout support via `overrideItemLayout` prop - `maintainVisibleContentPosition` enabled by default (no layout jumps) - Items can be dynamically resized without issues --- ## Pattern 2: Optimized FlatList ```typescript import { memo, useCallback, useMemo } from "react"; import { FlatList, View, Text, Pressable, Image, StyleSheet, Platform, type ListRenderItem, } from "react-native"; // Constants const ITEM_HEIGHT = 80; const SEPARATOR_HEIGHT = 1; const WINDOW_SIZE = 5; const MAX_TO_RENDER_PER_BATCH = 10; const INITIAL_NUM_TO_RENDER = 10; const ON_END_REACHED_THRESHOLD = 0.5; interface Product { id: string; name: string; price: number; imageUrl: string; } interface ProductItemProps { item: Product; onPress: (id: string) => void; } // Memoized item component - CRITICAL for performance const ProductItem = memo(function ProductItem({ item, onPress }: ProductItemProps) { const handlePress = useCallback(() => { onPress(item.id); }, [item.id, onPress]); return ( <Pressable onPress={handlePress} style={styles.item}> <Image source={{ uri: item.imageUrl }} style={styles.image} resizeMode="cover" /> <View style={styles.details}> <Text style={styles.name} numberOfLines={1}> {item.name} </Text> <Text style={styles.price}>${item.price.toFixed(2)}</Text> </View> </Pressable> ); }); // Separator component const ItemSeparator = memo(function ItemSeparator() { return <View style={styles.separator} />; }); // Main list component interface ProductListProps { products: Product[]; onProductPress: (id: string) => void; onEndReached?: () => void; isLoadingMore?: boolean; } export function ProductList({ products, onProductPress, onEndReached, isLoadingMore = false, }: ProductListProps) { const renderItem: ListRenderItem<Product> = useCallback( ({ item }) => <ProductItem item={item} onPress={onProductPress} />, [onProductPress] ); const keyExtractor = useCallback((item: Product) => item.id, []); // getItemLayout for fixed-height items (MAJOR performance win) const getItemLayout = useCallback( (_data: Product[] | null | undefined, index: number) => ({ length: ITEM_HEIGHT + SEPARATOR_HEIGHT, offset: (ITEM_HEIGHT + SEPARATOR_HEIGHT) * index, index, }), [] ); const ListFooter = useMemo(() => { if (!isLoadingMore) return null; return ( <View style={styles.footer}> <ActivityIndicator size="small" color="#007AFF" /> </View> ); }, [isLoadingMore]); return ( <FlatList data={products} renderItem={renderItem} keyExtractor={keyExtractor} getItemLayout={getItemLayout} ItemSeparatorComponent={ItemSeparator} ListFooterComponent={ListFooter} windowSize={WINDOW_SIZE} maxToRenderPerBatch={MAX_TO_RENDER_PER_BATCH} initialNumToRender={INITIAL_NUM_TO_RENDER} removeClippedSubviews={Platform.OS === "android"} showsVerticalScrollIndicator={false} onEndReached={onEndReached} onEndReachedThreshold={ON_END_REACHED_THRESHOLD} /> ); } const styles = StyleSheet.create({ item: { height: ITEM_HEIGHT, flexDirection: "row", alignItems: "center", paddingHorizontal: 16, backgroundColor: "#FFFFFF", }, image: { width: 60, height: 60, borderRadius: 8, backgroundColor: "#F2F2F7", }, details: { flex: 1, marginLeft: 12, }, name: { fontSize: 16, fontWeight: "600", color: "#1A1A1A", }, price: { fontSize: 14, color: "#007AFF", marginTop: 4, }, separator: { height: SEPARATOR_HEIGHT, backgroundColor: "#E5E5EA", marginLeft: 88, }, footer: { paddingVertical: 20, alignItems: "center", }, }); ``` --- ## Pattern 3: Memoization Patterns ```typescript import { memo, useMemo, useCallback, useRef } from "react"; // 1. React.memo for list items interface UserCardProps { user: User; onPress: (id: string) => void; } const UserCard = memo(function UserCard({ user, onPress }: UserCardProps) { const handlePress = useCallback(() => { onPress(user.id); }, [user.id, onPress]); return ( <Pressable onPress={handlePress}> <Text>{user.name}</Text> </Pressable> ); }); // 2. Custom comparison function for complex props const ExpensiveComponent = memo( function ExpensiveComponent({ data, config }: Props) { return <View>{/* ... */}</View>; }, (prevProps, nextProps) => { return ( prevProps.data.id === nextProps.data.id && prevProps.config.mode === nextProps.config.mode ); } ); // 3. useMemo for expensive computations function DataProcessor({ items, filters }: { items: Item[]; filters: Filters }) { const processedData = useMemo(() => { return items .filter((item) => { if (filters.category && item.category !== filters.category) return false; if (filters.minPrice && item.price < filters.minPrice) return false; if (filters.maxPrice && item.price > filters.maxPrice) return false; return true; }) .sort((a, b) => { switch (filters.sortBy) { case "price-asc": return a.price - b.price; case "price-desc": return b.price - a.price; case "name": return a.name.localeCompare(b.name); default: return 0; } }); }, [items, filters]); return <ItemList data={processedData} />; } // 4. Stable callbacks for memoized children function ParentWithStableCallbacks() { const [selectedId, setSelectedId] = useState<string | null>(null); const handleSelect = useCallback((id: string) => { setSelectedId(id); }, []); const handleAction = useCallback( (action: string) => { if (selectedId) { performAction(selectedId, action); } }, [selectedId] ); return ( <> <MemoizedList onSelect={handleSelect} /> <MemoizedActions onAction={handleAction} /> </> ); } ``` --- ## Pattern 4: Avoiding Common Performance Issues ```typescript // BAD: Inline style objects create new reference every render function BadComponent() { return ( <View style={{ padding: 16, margin: 8 }}> <Text style={{ fontSize: 16, color: "#000" }}>Hello</Text> </View> ); } // GOOD: Use StyleSheet.create const styles = StyleSheet.create({ container: { padding: 16, margin: 8 }, text: { fontSize: 16, color: "#000" }, }); function GoodComponent() { return ( <View style={styles.container}> <Text style={styles.text}>Hello</Text> </View> ); } // BAD: Inline function in FlatList <FlatList data={items} renderItem={({ item }) => <ItemCard item={item} onPress={() => handlePress(item.id)} />} /> // GOOD: Stable callbacks function GoodList({ items }: { items: Item[] }) { const handlePress = useCallback((id: string) => { // handle press }, []); const renderItem = useCallback( ({ item }: { item: Item }) => <MemoizedItemCard item={item} onPress={handlePress} />, [handlePress] ); return <FlatList data={items} renderItem={renderItem} />; } // BAD: New array reference for extraData <FlatList data={items} extraData={[selectedId, sortOrder]} // New array every render /> // GOOD: Memoized extraData function GoodListWithExtraData({ items, selectedId, sortOrder }: Props) { const extraData = useMemo(() => ({ selectedId, sortOrder }), [selectedId, sortOrder]); return <FlatList data={items} extraData={extraData} />; } ``` --- ## Pattern 5: SectionList Optimization ```typescript import { memo, useCallback } from "react"; import { SectionList, Text, View, StyleSheet } from "react-native"; const SECTION_HEADER_HEIGHT = 40; const ITEM_HEIGHT = 60; interface Section { title: string; data: Item[]; } const SectionHeader = memo(function SectionHeader({ title }: { title: string }) { return ( <View style={styles.sectionHeader}> <Text style={styles.sectionTitle}>{title}</Text> </View> ); }); const SectionItem = memo(function SectionItem({ item, onPress, }: { item: Item; onPress: (id: string) => void; }) { const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]); return ( <Pressable onPress={handlePress} style={styles.item}> <Text>{item.name}</Text> </Pressable> ); }); export function OptimizedSectionList({ sections, onItemPress }: { sections: Section[]; onItemPress: (id: string) => void; }) { const renderSectionHeader = useCallback( ({ section }: { section: Section }) => <SectionHeader title={section.title} />, [] ); const renderItem = useCallback( ({ item }: { item: Item }) => <SectionItem item={item} onPress={onItemPress} />, [onItemPress] ); const keyExtractor = useCallback((item: Item) => item.id, []); return ( <SectionList sections={sections} renderSectionHeader={renderSectionHeader} renderItem={renderItem} keyExtractor={keyExtractor} stickySectionHeadersEnabled initialNumToRender={15} maxToRenderPerBatch={10} windowSize={5} /> ); } const styles = StyleSheet.create({ sectionHeader: { height: SECTION_HEADER_HEIGHT, backgroundColor: "#F2F2F7", justifyContent: "center", paddingHorizontal: 16, }, sectionTitle: { fontSize: 14, fontWeight: "600", color: "#8E8E93", textTransform: "uppercase", }, item: { height: ITEM_HEIGHT, justifyContent: "center", paddingHorizontal: 16, backgroundColor: "#FFFFFF", borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: "#E5E5EA", }, }); ``` --- ## Pattern 6: Lazy Loading Screens ```typescript import { lazy, Suspense } from "react"; import { ActivityIndicator, View, StyleSheet } from "react-native"; // Lazy load heavy screens const HeavyDashboard = lazy(() => import("./screens/heavy-dashboard")); const AnalyticsScreen = lazy(() => import("./screens/analytics")); // Loading fallback component function ScreenLoader() { return ( <View style={styles.loader}> <ActivityIndicator size="large" color="#007AFF" /> </View> ); } // Wrapper for lazy screens function LazyScreen({ component: Component, ...props }: { component: React.LazyExoticComponent<any> }) { return ( <Suspense fallback={<ScreenLoader />}> <Component {...props} /> </Suspense> ); } // In navigator function MainNavigator() { return ( <Stack.Navigator> <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="Dashboard"> {(props) => <LazyScreen component={HeavyDashboard} {...props} />} </Stack.Screen> <Stack.Screen name="Analytics"> {(props) => <LazyScreen component={AnalyticsScreen} {...props} />} </Stack.Screen> </Stack.Navigator> ); } const styles = StyleSheet.create({ loader: { flex: 1, justifyContent: "center", alignItems: "center", }, }); ``` --- ## Pattern 7: Performance Monitoring Hook ```typescript import { useEffect, useRef } from "react"; const RENDER_THRESHOLD = 10; const TIME_THRESHOLD_MS = 16; // 60fps = 16ms per frame export function usePerformanceMonitor(componentName: string) { const renderCount = useRef(0); const lastRenderTime = useRef(Date.now()); useEffect(() => { renderCount.current += 1; const now = Date.now(); const timeSinceLastRender = now - lastRenderTime.current; lastRenderTime.current = now; if (__DEV__) { if (renderCount.current > RENDER_THRESHOLD) { console.warn( `[Performance] ${componentName} has rendered ${renderCount.current} times` ); } if (timeSinceLastRender < TIME_THRESHOLD_MS && renderCount.current > 1) { console.warn( `[Performance] ${componentName} rendered twice within ${timeSinceLastRender}ms` ); } } }); useEffect(() => { return () => { if (__DEV__ && renderCount.current > RENDER_THRESHOLD) { console.log( `[Performance] ${componentName} total renders: ${renderCount.current}` ); } }; }, [componentName]); } // Usage function ExpensiveComponent() { usePerformanceMonitor("ExpensiveComponent"); return <View>{/* ... */}</View>; } ``` -
styling.md 11.8 KB
# React Native - Styling Patterns > StyleSheet patterns, design tokens, platform-specific styling, and theming. See [core.md](core.md) for component architecture. **Prerequisites**: Understand [Pattern 3: Platform-Specific Code](../SKILL.md) from SKILL.md. --- ## Pattern 1: Design Tokens ```typescript // constants/design-tokens.ts // Spacing scale (4px base) export const SPACING = { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48, } as const; // Typography export const FONT_SIZE = { xs: 12, sm: 14, md: 16, lg: 18, xl: 24, xxl: 32, xxxl: 40, } as const; export const FONT_WEIGHT = { regular: "400" as const, medium: "500" as const, semibold: "600" as const, bold: "700" as const, }; // Colors - Light theme export const COLORS = { // Brand primary: "#007AFF", primaryLight: "#4DA3FF", primaryDark: "#0056B3", secondary: "#5856D6", // Semantic success: "#34C759", warning: "#FF9500", error: "#FF3B30", info: "#5AC8FA", // Neutral background: "#FFFFFF", surface: "#F2F2F7", surfaceElevated: "#FFFFFF", // Text text: "#000000", textSecondary: "#3C3C43", textTertiary: "#8E8E93", textInverse: "#FFFFFF", // Border border: "#C6C6C8", borderLight: "#E5E5EA", // Transparent overlay: "rgba(0,0,0,0.4)", transparent: "transparent", } as const; // Border radius export const RADIUS = { sm: 4, md: 8, lg: 12, xl: 16, full: 9999, } as const; // Shadows (iOS only - use ELEVATIONS for Android) export const SHADOWS = { sm: { shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 2, }, md: { shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, }, lg: { shadowColor: "#000", shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.15, shadowRadius: 8, }, } as const; // Elevations (Android only - use SHADOWS for iOS) export const ELEVATIONS = { sm: 2, md: 4, lg: 8, } as const; ``` --- ## Pattern 2: StyleSheet with Design Tokens ```typescript import { StyleSheet, Platform, type ViewStyle, type TextStyle, } from "react-native"; import { COLORS, SPACING, FONT_SIZE, RADIUS, SHADOWS, ELEVATIONS, } from "../constants/design-tokens"; // Type-safe styles interface interface CardStyles { container: ViewStyle; header: ViewStyle; title: TextStyle; description: TextStyle; footer: ViewStyle; } export const cardStyles = StyleSheet.create<CardStyles>({ container: { backgroundColor: COLORS.surfaceElevated, borderRadius: RADIUS.lg, padding: SPACING.md, ...Platform.select({ ios: SHADOWS.md, android: { elevation: ELEVATIONS.md }, }), }, header: { flexDirection: "row", alignItems: "center", marginBottom: SPACING.sm, }, title: { fontSize: FONT_SIZE.lg, fontWeight: "600", color: COLORS.text, }, description: { fontSize: FONT_SIZE.md, color: COLORS.textSecondary, lineHeight: FONT_SIZE.md * 1.5, }, footer: { marginTop: SPACING.md, paddingTop: SPACING.sm, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: COLORS.borderLight, flexDirection: "row", justifyContent: "flex-end", }, }); ``` --- ## Pattern 3: New Architecture Style Props (React Native 0.76+) The New Architecture introduces `boxShadow` and `filter` props that work cross-platform. ```typescript import { View, StyleSheet } from "react-native"; const styles = StyleSheet.create({ // boxShadow - works on BOTH iOS and Android (New Architecture only) cardWithBoxShadow: { backgroundColor: "#FFFFFF", borderRadius: 12, padding: 16, // String syntax (CSS-like) boxShadow: "5 5 5 0 rgba(0, 0, 0, 0.2)", // OR object syntax: // boxShadow: { // offsetX: 5, // offsetY: 5, // blurRadius: 5, // spreadDistance: 0, // color: "rgba(0, 0, 0, 0.2)", // }, }, // Inset shadow (New Architecture only, Android 10+) insetShadowCard: { boxShadow: "inset 0 2 4 0 rgba(0, 0, 0, 0.1)", }, // filter - apply visual effects (New Architecture only) blurredOverlay: { filter: "blur(10)", }, grayscaleImage: { filter: "grayscale(1)", }, // Multiple filters combinedFilters: { filter: "saturate(0.5) brightness(1.2)", // OR array syntax: // filter: [{ saturate: 0.5 }, { brightness: 1.2 }], }, }); // NOTE: filter implies overflow: hidden and clips children outside parent bounds // NOTE: iOS filter only supports brightness and opacity // NOTE: Android blur and dropShadow require Android 12+ ``` **Why use boxShadow over legacy shadow props:** - Works on both iOS AND Android (legacy shadow props are iOS-only) - Supports inset shadows - Includes spread parameter - Works on Views without background color - Web-aligned syntax --- ## Pattern 4: Platform-Specific Styling ```typescript import { StyleSheet, Platform } from "react-native"; import { COLORS, SHADOWS, ELEVATIONS, FONT_WEIGHT } from "../constants/design-tokens"; const styles = StyleSheet.create({ card: { backgroundColor: COLORS.surfaceElevated, borderRadius: 12, padding: 16, // Platform-specific shadows ...Platform.select({ ios: { ...SHADOWS.md, }, android: { elevation: ELEVATIONS.md, }, }), }, text: { // Platform-specific fonts fontFamily: Platform.select({ ios: "San Francisco", android: "Roboto", default: "System", }), // Android only reliably supports normal/bold fontWeight: Platform.select({ ios: FONT_WEIGHT.semibold, android: FONT_WEIGHT.bold, }), }, button: { // Platform-specific hit slop ...Platform.select({ ios: { paddingVertical: 12, }, android: { paddingVertical: 14, // Slightly larger for Android touch targets }, }), }, }); // Platform-specific component props function PlatformButton({ onPress, children }: ButtonProps) { return ( <Pressable onPress={onPress} style={styles.button} android_ripple={Platform.OS === "android" ? { color: "rgba(0,0,0,0.1)" } : undefined} > {children} </Pressable> ); } ``` --- ## Pattern 5: Theming with Context ```typescript // context/theme-context.tsx import { createContext, useContext, useState, useMemo, type ReactNode } from "react"; import { useColorScheme } from "react-native"; // Theme types interface Theme { colors: { primary: string; background: string; surface: string; text: string; textSecondary: string; border: string; }; spacing: typeof SPACING; radius: typeof RADIUS; } const lightTheme: Theme = { colors: { primary: "#007AFF", background: "#FFFFFF", surface: "#F2F2F7", text: "#000000", textSecondary: "#3C3C43", border: "#C6C6C8", }, spacing: SPACING, radius: RADIUS, }; const darkTheme: Theme = { colors: { primary: "#0A84FF", background: "#000000", surface: "#1C1C1E", text: "#FFFFFF", textSecondary: "#EBEBF5", border: "#38383A", }, spacing: SPACING, radius: RADIUS, }; // Context interface ThemeContextValue { theme: Theme; isDark: boolean; toggleTheme: () => void; setTheme: (mode: "light" | "dark" | "system") => void; } const ThemeContext = createContext<ThemeContextValue | null>(null); // Provider export function ThemeProvider({ children }: { children: ReactNode }) { const systemColorScheme = useColorScheme(); const [mode, setMode] = useState<"light" | "dark" | "system">("system"); const isDark = useMemo(() => { if (mode === "system") { return systemColorScheme === "dark"; } return mode === "dark"; }, [mode, systemColorScheme]); const theme = isDark ? darkTheme : lightTheme; const toggleTheme = () => { setMode((current) => (current === "dark" ? "light" : "dark")); }; const value = useMemo( () => ({ theme, isDark, toggleTheme, setTheme: setMode }), [theme, isDark] ); return ( <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider> ); } // Hook export function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error("useTheme must be used within ThemeProvider"); } return context; } // Styled component using theme function ThemedCard({ children }: { children: ReactNode }) { const { theme } = useTheme(); return ( <View style={{ backgroundColor: theme.colors.surface, borderRadius: theme.radius.lg, padding: theme.spacing.md, borderWidth: 1, borderColor: theme.colors.border, }} > {children} </View> ); } ``` --- ## Pattern 6: Dynamic Style Arrays with Variants ```typescript import { StyleSheet, View, Text, type ViewStyle, type TextStyle } from "react-native"; import { useMemo } from "react"; interface BadgeProps { children: string; variant?: "default" | "success" | "warning" | "error"; size?: "sm" | "md" | "lg"; outlined?: boolean; style?: ViewStyle; } const BADGE_COLORS = { default: { bg: "#E5E5EA", text: "#3C3C43" }, success: { bg: "#D1FAE5", text: "#065F46" }, warning: { bg: "#FEF3C7", text: "#92400E" }, error: { bg: "#FEE2E2", text: "#991B1B" }, } as const; const BADGE_SIZES = { sm: { paddingVertical: 2, paddingHorizontal: 6, fontSize: 10 }, md: { paddingVertical: 4, paddingHorizontal: 8, fontSize: 12 }, lg: { paddingVertical: 6, paddingHorizontal: 12, fontSize: 14 }, } as const; export function Badge({ children, variant = "default", size = "md", outlined = false, style, }: BadgeProps) { const containerStyle = useMemo<ViewStyle[]>( () => [ styles.base, { paddingVertical: BADGE_SIZES[size].paddingVertical, paddingHorizontal: BADGE_SIZES[size].paddingHorizontal, backgroundColor: outlined ? "transparent" : BADGE_COLORS[variant].bg, borderWidth: outlined ? 1 : 0, borderColor: BADGE_COLORS[variant].bg, }, style, ].filter(Boolean) as ViewStyle[], [variant, size, outlined, style] ); const textStyle = useMemo<TextStyle[]>( () => [ styles.text, { fontSize: BADGE_SIZES[size].fontSize, color: BADGE_COLORS[variant].text, }, ], [variant, size] ); return ( <View style={containerStyle}> <Text style={textStyle}>{children}</Text> </View> ); } const styles = StyleSheet.create({ base: { borderRadius: 9999, alignSelf: "flex-start", }, text: { fontWeight: "600", textTransform: "uppercase", }, }); ``` --- ## Pattern 7: Responsive Styling ```typescript import { useWindowDimensions } from "react-native"; // Constants for breakpoints const BREAKPOINTS = { sm: 640, md: 768, lg: 1024, xl: 1280, } as const; // Hook for responsive values function useResponsiveValue<T>(values: { default: T; sm?: T; md?: T; lg?: T; xl?: T }): T { const { width } = useWindowDimensions(); if (width >= BREAKPOINTS.xl && values.xl !== undefined) return values.xl; if (width >= BREAKPOINTS.lg && values.lg !== undefined) return values.lg; if (width >= BREAKPOINTS.md && values.md !== undefined) return values.md; if (width >= BREAKPOINTS.sm && values.sm !== undefined) return values.sm; return values.default; } // Usage function ResponsiveGrid({ items }: { items: Item[] }) { const numColumns = useResponsiveValue({ default: 2, sm: 2, md: 3, lg: 4, xl: 5, }); const itemPadding = useResponsiveValue({ default: 8, md: 16, lg: 24, }); return ( <FlatList data={items} numColumns={numColumns} key={numColumns} // Re-render when columns change contentContainerStyle={{ padding: itemPadding }} renderItem={({ item }) => ( <View style={{ flex: 1 / numColumns, padding: itemPadding / 2 }}> <ItemCard item={item} /> </View> )} /> ); } ```
-
-
reference.md 8.9 KB
# React Native Reference > Decision frameworks, checklists, and quick reference. See [SKILL.md](SKILL.md) for red flags and anti-patterns. --- ## Decision Framework ### New Architecture (React Native 0.76+) ``` Is your app on React Native 0.76+? ├─ YES → New Architecture is ENABLED by default │ └─ Check library compatibility (90%+ of popular libs support it) └─ NO → Consider upgrading (legacy architecture is deprecated with warnings) Need to opt out temporarily? ├─ Add to app.json (Expo): newArchEnabled: false ├─ Add to gradle.properties (bare): newArchEnabled=false └─ WARNING: App will crash in 0.78+ if legacy architecture is forced Are you using Reanimated? ├─ Reanimated 4.x → New Architecture ONLY (requires react-native-worklets) └─ Reanimated 3.x → Supports both architectures (still maintained) ``` ### Expo vs Bare Workflow ``` Starting a new React Native project? ├─ Need custom native modules (not in Expo SDK)? │ ├─ YES → Bare workflow or Expo with development builds │ └─ NO → Continue... ├─ App size critical (<15MB)? │ ├─ YES → Bare workflow (Expo adds overhead) │ └─ NO → Continue... ├─ Team has native (iOS/Android) expertise? │ ├─ YES → Either (lean bare if complex native work) │ └─ NO → Expo (handles native complexity) ├─ Need OTA updates? │ ├─ YES → Expo (EAS Update) │ └─ NO → Either └─ Default → Expo (95% of cases) ``` ### List Component Choice ``` How many items in the list? ├─ < 10 items → ScrollView + map() is fine ├─ 10-50 items → FlashList or FlatList recommended ├─ 50+ items → FlashList STRONGLY recommended (or FlatList REQUIRED) └─ 1000+ items → FlashList v2 (best performance) Are you on New Architecture (0.76+)? ├─ YES → FlashList v2 (auto-sizes items, 50% less blank area) │ └─ No estimatedItemSize needed (measures real items) └─ NO → FlashList v1 or FlatList └─ estimatedItemSize REQUIRED for FlashList v1 Do items have variable heights? ├─ FlashList v2 → Handles automatically, items can resize dynamically ├─ FlashList v1 → Provide estimatedItemSize or overrideItemLayout └─ FlatList → Cannot use getItemLayout (performance hit) Need sections with headers? ├─ YES → SectionList (or FlashList with getItemType) └─ NO → FlashList or FlatList FlashList v2 vs FlatList Decision: ├─ Complex items, low-end Android → FlashList v2 (cell recycling) ├─ Simple items, high-end devices → Either works ├─ Need masonry layout → FlashList v2 (built-in support) ├─ Need maintainVisibleContentPosition → FlashList v2 (enabled by default) └─ Default recommendation → FlashList v2 (better performance) ``` ### Styling Approach ``` Does component have variants (primary/secondary, sm/md/lg)? ├─ YES → StyleSheet with computed styles or style arrays │ OR → Use your variant styling solution └─ NO → StyleSheet.create for static styles Are values dynamic (runtime values like theme colors)? ├─ YES → Inline styles or style arrays └─ NO → StyleSheet.create (better performance) ``` ### Navigation Pattern ``` What type of navigation flow? ├─ Linear flow (onboarding, checkout) → Stack Navigator ├─ Main app sections → Tab Navigator (bottom tabs) ├─ Settings/menu → Drawer Navigator ├─ Modals → Stack with presentation: 'modal' └─ Deep linking required → Configure linking config React Navigation 7+ API Choice: ├─ Simple app, TypeScript-first → Static API (less boilerplate) ├─ Complex dynamic navigation → Dynamic API (more flexible) ├─ Mix of both → Use static for top-level, dynamic for nested └─ File-based routing → Use your managed workflow's router (built on React Navigation) Auth flow pattern? ├─ Switch between auth/main navigators based on auth state └─ Don't conditionally render screens in same navigator ``` ### Memoization Decision ``` Should I memoize this? ├─ Is it a FlatList renderItem callback? │ └─ YES → Always useCallback ├─ Is it a component receiving stable props? │ └─ YES → Consider React.memo ├─ Is computation expensive (>5ms)? │ └─ YES → useMemo ├─ Is it a callback passed to memoized child? │ └─ YES → useCallback └─ Default → Don't memoize (premature optimization) ``` --- ## File Organization Reference ### Recommended Directory Structure ``` src/ ├── app/ # File-based routes (if using managed workflow) ├── screens/ # Screen components │ ├── home/ │ │ ├── home-screen.tsx │ │ └── components/ # Screen-specific components │ └── auth/ │ ├── login-screen.tsx │ └── register-screen.tsx ├── components/ # Shared components │ ├── ui/ # Base UI components │ │ ├── button.tsx │ │ ├── input.tsx │ │ └── card.tsx │ └── features/ # Feature-specific components ├── navigation/ # Navigation configuration │ ├── root-navigator.tsx │ ├── auth-navigator.tsx │ └── types.ts # Navigation type definitions ├── hooks/ # Custom hooks ├── stores/ # State management stores ├── services/ # API services ├── utils/ # Utility functions ├── constants/ # App constants, design tokens └── types/ # Shared TypeScript types ``` ### File Naming Conventions ``` Components: kebab-case.tsx (e.g., user-profile.tsx) Screens: kebab-case-screen.tsx (e.g., home-screen.tsx) Hooks: use-kebab-case.ts (e.g., use-auth.ts) Stores: kebab-case-store.ts (e.g., auth-store.ts) Utils: kebab-case.ts (e.g., format-date.ts) Types: types.ts or kebab-case.types.ts Platform files: name.ios.tsx, name.android.tsx ``` --- ## Performance Checklist ### FlatList Optimization - [ ] Using FlatList (not ScrollView + map) for 20+ items - [ ] renderItem wrapped in useCallback - [ ] keyExtractor returns stable unique ID (not index) - [ ] getItemLayout provided if items have fixed height - [ ] initialNumToRender, maxToRenderPerBatch, windowSize tuned - [ ] removeClippedSubviews={true} on Android for memory - [ ] Item components wrapped in React.memo ### Component Optimization - [ ] No inline styles for static values (use StyleSheet) - [ ] No inline functions passed to memoized children - [ ] useMemo for expensive computations (>5ms) - [ ] useCallback for callbacks passed to children - [ ] React.memo on frequently re-rendering pure components ### Image Optimization - [ ] Using an optimized image library for heavy image usage - [ ] Images sized appropriately (not 4K images in thumbnails) - [ ] Preloading critical images - [ ] Using appropriate resizeMode - [ ] Caching enabled for network images ### Navigation Optimization - [ ] Lazy loading heavy screens - [ ] Screen preloading for likely next screens - [ ] useFocusEffect for resource setup/cleanup - [ ] Avoiding inline component functions in Screen definitions --- ## Quick Reference ### Essential Imports ```typescript // Core React Native import { View, Text, Pressable, FlatList, StyleSheet, Platform, Dimensions, KeyboardAvoidingView, ScrollView, Image, TextInput, ActivityIndicator, } from "react-native"; // Safe Area import { SafeAreaView, useSafeAreaInsets, } from "react-native-safe-area-context"; // Navigation import { useNavigation, useRoute, useFocusEffect, } from "@react-navigation/native"; import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; // Animations import Animated, { useSharedValue, useAnimatedStyle, withSpring, withTiming, } from "react-native-reanimated"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; ``` ### Common TypeScript Patterns ```typescript // Navigation types type RootStackParamList = { Home: undefined; Profile: { userId: string }; Settings: { section?: string }; }; type NavigationProp = NativeStackNavigationProp<RootStackParamList>; // Component props interface ComponentProps { children: React.ReactNode; style?: ViewStyle; testID?: string; } // FlatList renderItem type RenderItem<T> = ({ item, index, }: { item: T; index: number; }) => React.ReactElement; ``` ### CLI Commands ```bash # React Native CLI (bare workflow) npx react-native start # Start Metro bundler npx react-native run-ios # Run on iOS simulator npx react-native run-android # Run on Android emulator cd ios && pod install # Install iOS dependencies ``` > For managed workflow CLI commands (start, build, update), see your managed workflow's skill documentation. -
SKILL.md 12.9 KB
--- name: mobile-framework-react-native description: React Native mobile development patterns - New Architecture (Fabric, TurboModules, JSI), component architecture, React Navigation 7+, FlashList v2 optimization, gestures with Reanimated 4, platform-specific code, React 19 features --- # React Native Development Patterns > **Quick Guide:** Build cross-platform mobile apps with React Native's New Architecture (default since 0.76). Use FlashList for performant lists (or FlatList with proper optimization). Use type-safe navigation hooks with static or dynamic API. Keep components small, memoize callbacks passed to lists, and test on both platforms from day one. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use FlashList (preferred) or FlatList for lists with more than 20 items - NEVER ScrollView with .map() for long lists)** **(You MUST memoize renderItem callbacks and use stable keyExtractor functions - avoid key props on FlashList items as it breaks recycling)** **(You MUST use react-native-safe-area-context for safe areas - React Native's built-in SafeAreaView is deprecated in 0.81+ and will be removed)** **(You MUST test on BOTH iOS AND Android from day one - platform differences cause bugs)** **(You MUST use Platform.select() or platform-specific files for platform differences - shadows, fonts, and feedback differ)** **(You MUST be aware the New Architecture is enabled by default since React Native 0.76 - Fabric, TurboModules, and bridgeless mode)** </critical_requirements> --- **Auto-detection:** React Native, react-native, React Navigation, @react-navigation, StyleSheet, FlatList, FlashList, ScrollView, View, Text, Pressable, TouchableOpacity, Platform.OS, Platform.select, SafeAreaView, KeyboardAvoidingView, Reanimated, Gesture Handler, TurboModules, Fabric, JSI, New Architecture **When to use:** - Building cross-platform iOS and Android mobile applications - Creating native mobile UIs with React patterns - Implementing mobile navigation with stack, tab, or drawer patterns - Optimizing list performance with FlashList/FlatList and virtualization - Adding gestures and animations with Reanimated 4 - Handling platform-specific code for iOS vs Android differences - Working with React Native's New Architecture (Fabric, TurboModules, JSI) **Key patterns covered:** - New Architecture fundamentals (Fabric, TurboModules, JSI, bridgeless mode) - Component architecture with accessibility props and platform-specific patterns - React Navigation 7+ with type-safe hooks, static API, and auth flows - FlashList/FlatList optimization with memoization and cell recycling - Platform-specific code with Platform.select and file extensions - Safe area and keyboard handling - React 19 features (React Native 0.78+): useOptimistic, `use`, ref as props **When NOT to use:** - Web-only React applications (use standard React patterns) - React Native Web hybrid apps (requires additional considerations) - Flutter, Swift, or Kotlin native development **Detailed Resources:** - [examples/core.md](examples/core.md) - Component architecture, compound components - [examples/navigation.md](examples/navigation.md) - Type-safe navigation, auth flows, deep linking - [examples/styling.md](examples/styling.md) - StyleSheet, design tokens, theming, responsive styling - [examples/performance.md](examples/performance.md) - FlashList, FlatList optimization, memoization - [reference.md](reference.md) - Decision frameworks, checklists, CLI commands --- <philosophy> ## Philosophy React Native enables building native mobile apps using React patterns. The key insight is that **mobile has different constraints than web**: performance matters more, platform conventions differ, and users expect native feel. **Core principles:** 1. **New Architecture first** - React Native 0.76+ uses the New Architecture by default (Fabric, TurboModules, JSI, bridgeless mode) 2. **Platform-first thinking** - iOS and Android have different UX conventions (haptics, ripples, navigation patterns) 3. **Performance by default** - Mobile devices are constrained; use FlashList/FlatList, memoize callbacks, avoid inline styles 4. **Native feel matters** - Use native components, proper keyboard handling, safe area insets 5. **Type safety prevents bugs** - Type navigation params, props, and native module interfaces 6. **Test both platforms early** - Platform bugs compound over time; test daily on both **New Architecture (React Native 0.76+):** The New Architecture removes the legacy bridge and provides: - **Fabric** - Modern rendering engine with synchronous layout effects - **TurboModules** - Lazy-loaded native modules with type-safe interfaces - **JSI (JavaScript Interface)** - Direct synchronous JS-to-native calls without serialization - **Bridgeless Mode** - Complete removal of the async bridge for better performance Performance improvements with New Architecture: ~15ms faster app startup (~8% improvement), ~3.8MB smaller app size (20% reduction), ~15x faster Metro resolver, ~4x faster warm builds. **Mental model:** React Native is NOT "write once, run anywhere" - it's "learn once, write anywhere." Expect to write some platform-specific code. The shared codebase is typically 80-95%, not 100%. </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Component Architecture Build components with TypeScript, accessibility props, and platform awareness. Key concerns: use `Pressable` over `TouchableOpacity`, include `accessibilityRole`/`accessibilityState`, use `testID` for E2E tests, and memoize styles with `useMemo`. ```typescript // Key pattern: accessibility + testID + memoized styles <Pressable ref={ref} style={buttonStyle} onPress={handlePress} disabled={disabled} testID={testID} accessibilityRole="button" accessibilityState={{ disabled }} > <Text style={styles.text}>{children}</Text> </Pressable> ``` **Why good:** accessibilityRole/State for screen readers, testID for E2E, Pressable supports android_ripple unlike TouchableOpacity See [examples/core.md](examples/core.md) for full component with forwardRef, variants, and loading state. --- ### Pattern 2: Safe Area and Keyboard Handling Handle device notches, status bars, and keyboard properly. Use `react-native-safe-area-context` (RN's built-in SafeAreaView is deprecated in 0.81+). ```typescript import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context"; // Screen wrapper with safe areas <SafeAreaView style={{ flex: 1 }} edges={["top", "bottom"]}> {children} </SafeAreaView> // Keyboard handling for forms <KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === "ios" ? "padding" : "height"} keyboardVerticalOffset={Platform.select({ ios: HEADER_HEIGHT, android: 0 })} > <ScrollView keyboardShouldPersistTaps="handled"> <FormContent /> </ScrollView> </KeyboardAvoidingView> ``` **Why good:** SafeAreaView handles notches/Dynamic Island, KeyboardAvoidingView prevents keyboard overlap, Platform.select handles iOS/Android differences --- ### Pattern 3: Platform-Specific Code Handle iOS and Android differences with `Platform.select` for small differences, separate files (`.ios.tsx`/`.android.tsx`) for significant divergence. ```typescript // Platform.select for shadows (iOS shadow props ignored on Android) const styles = StyleSheet.create({ card: { ...Platform.select({ ios: { shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, }, android: { elevation: 4 }, }), }, text: { fontWeight: Platform.select({ ios: "600", android: "bold" }), }, }); ``` **Why good:** iOS shadow props are completely ignored on Android, fontWeight 100-900 unreliable on Android For platform-specific file splitting with haptics, see [examples/core.md](examples/core.md). --- ### Pattern 4: FlashList and FlatList Optimization Use FlashList for performant lists (cell recycling, 50% less blank area). FlashList v2 is New Architecture only. Always memoize renderItem and keyExtractor. Never add `key` props to FlashList items (breaks recycling). ```typescript // Critical: memoized renderItem + no key prop on items const renderItem = useCallback( ({ item }: { item: Product }) => ( <ProductItem item={item} onPress={onProductPress} /> ), [onProductPress] ); <FlashList data={products} renderItem={renderItem} estimatedItemSize={ITEM_HEIGHT} // Optional in v2, required in v1 getItemType={(item) => item.category} // Improves recycling pool /> ``` **Why good:** useCallback prevents renderItem recreation, getItemType optimizes recycling, no key prop preserves FlashList's main benefit See [examples/performance.md](examples/performance.md) for full FlashList and FlatList optimization patterns. --- ### Pattern 5: React 19 Features (React Native 0.78+) React Native 0.78+ includes React 19 with new hooks and simplified patterns. ```typescript import { useOptimistic } from "react"; // useOptimistic - optimistic UI updates with automatic rollback const [optimisticMessages, addOptimisticMessage] = useOptimistic( messages, (state, newMessage: Message) => [...state, { ...newMessage, sending: true }] ); // ref as props - no more forwardRef wrapper needed (React 19) function Input({ ref, placeholder, onChangeText }: InputProps) { return <TextInput ref={ref} placeholder={placeholder} onChangeText={onChangeText} />; } ``` **Why good:** useOptimistic provides automatic rollback on errors, ref as props eliminates forwardRef boilerplate </patterns> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Using ScrollView + map() for lists with 50+ items - causes severe performance, use FlashList or FlatList - Adding key prop to FlashList items - BREAKS cell recycling, eliminates FlashList's main benefit - Using React Native's built-in SafeAreaView - deprecated in 0.81+, use react-native-safe-area-context - Not testing on both platforms - iOS/Android differences compound; test daily on both - Inline functions in FlatList/FlashList renderItem - creates new function every render, breaks memoization - Using Reanimated 4 with old architecture - Reanimated 4.x is New Architecture ONLY **Medium Priority Issues:** - Hardcoded colors/spacing instead of constants - breaks consistency, makes theming impossible - Not using Platform.select for shadows - iOS shadow props don't work on Android (use elevation) - Missing keyboard handling on forms - keyboard covers inputs without KeyboardAvoidingView - Using TouchableOpacity everywhere - Pressable is more flexible and supports android_ripple **Gotchas & Edge Cases:** - Android fontWeight only supports 'normal' and 'bold' reliably - 100-900 values may not work - iOS shadow props are completely ignored on Android - must use elevation for Android shadows - StatusBar backgroundColor only works on Android - iOS uses translucent status bar - FlatList onEndReached fires immediately if data fits screen - use onEndReachedThreshold carefully - KeyboardAvoidingView behavior differs: 'padding' for iOS, 'height' for Android - React Native doesn't have CSS cascade - each component must have complete styles - Text must be wrapped in `<Text>` component - raw strings cause crashes - New Architecture enabled by default in 0.76+ - some older libraries may need updates - FlashList v2 is New Architecture only - use v1 or FlatList if on old architecture - React Native 0.78+ uses React 19 - propTypes removed, forwardRef optional - React 19 adoption in recent SDK versions - check for breaking changes in your dependencies - Android 15/16 enforces edge-to-edge - must handle safe areas properly - Reanimated 4 requires react-native-worklets - Reanimated 3 will not work with it installed - boxShadow and filter props are New Architecture only - not available on legacy architecture - Reanimated 4: withSpring no longer uses restDisplacementThreshold/restSpeedThreshold - replaced by energyThreshold </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** **(You MUST use FlashList (preferred) or FlatList for lists with more than 20 items - NEVER ScrollView with .map() for long lists)** **(You MUST memoize renderItem callbacks and use stable keyExtractor functions - avoid key props on FlashList items as it breaks recycling)** **(You MUST use react-native-safe-area-context for safe areas - React Native's built-in SafeAreaView is deprecated in 0.81+ and will be removed)** **(You MUST test on BOTH iOS AND Android from day one - platform differences cause bugs)** **(You MUST use Platform.select() or platform-specific files for platform differences - shadows, fonts, and feedback differ)** **(You MUST be aware the New Architecture is enabled by default since React Native 0.76 - Fabric, TurboModules, and bridgeless mode)** **Failure to follow these rules will result in poor performance, platform-specific bugs, and broken UX on mobile devices.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.