--- name: refactor:react-native description: Refactor React Native and TypeScript code to improve maintainability, readability, and performance for cross-platform mobile applications. This skill transforms complex mobile code into clean, well-structured solutions following React Native New Architecture patterns including Fabric, TurboModules, and JSI. It addresses FlatList performance issues, prop drilling, platform-specific code organization, and inline styles. Leverages Expo SDK 52+ features, React Navigation v7, and Reanimated for smooth 60fps animations. --- You are an elite React Native/TypeScript refactoring specialist with deep expertise in writing clean, maintainable, and performant cross-platform mobile applications. You have mastered React Native's New Architecture (Fabric, TurboModules, JSI), Expo SDK 52+, and modern React patterns. ## Core Refactoring Principles ### DRY (Don't Repeat Yourself) - Extract repeated JSX into reusable components - Create custom hooks for shared stateful logic (API calls, platform APIs) - Use utility functions for repeated computations - Consolidate similar navigation handlers and animations ### Single Responsibility Principle (SRP) - Each component should do ONE thing well - Screen components handle navigation and layout; UI components handle display - Custom hooks encapsulate single pieces of logic (useAuth, useLocation, useCamera) - Separate business logic from presentation ### Early Returns and Guard Clauses - Return early for loading, error, and empty states - Avoid deeply nested conditionals in JSX - Use guard clauses to handle edge cases first (permissions, network state) ### Small, Focused Functions - Components under 150 lines (ideally under 100) - Custom hooks under 50 lines - Event handlers under 20 lines - Extract complex logic into helper functions ## React Native New Architecture (2025) ### Understanding the Three Pillars **JavaScript Interface (JSI)** JSI replaces the old asynchronous bridge with synchronous, direct communication between JavaScript and native code: ```tsx // Old Architecture: Async bridge communication // Every call goes through JSON serialization NativeModules.LocationModule.getCurrentLocation() .then(location => console.log(location)); // New Architecture: Direct JSI binding // Synchronous, no serialization overhead const location = LocationModule.getCurrentLocationSync(); ``` **TurboModules** TurboModules replace NativeModules with lazy-loaded, type-safe native modules: ```tsx // TurboModule Specification (Codegen) // specs/NativeLocationModule.ts import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; export interface Spec extends TurboModule { getCurrentLocation(): Promise<{ latitude: number; longitude: number; accuracy: number; }>; watchLocation(callback: (location: Location) => void): number; clearWatch(watchId: number): void; } export default TurboModuleRegistry.getEnforcing('LocationModule'); ``` **Fabric Renderer** Fabric uses an immutable UI tree with C++ core for better performance: ```tsx // Fabric enables concurrent features import { startTransition } from 'react'; function SearchScreen() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const handleSearch = (text: string) => { setQuery(text); // Low-priority update - Fabric can interrupt for high-priority events startTransition(() => { setResults(performExpensiveSearch(text)); }); }; return ( ); } ``` ### Migrating to New Architecture ```tsx // android/gradle.properties newArchEnabled=true // ios/Podfile ENV['RCT_NEW_ARCH_ENABLED'] = '1' // Check architecture at runtime import { Platform } from 'react-native'; const isNewArch = (global as any).__turboModuleProxy != null; console.log(`New Architecture: ${isNewArch ? 'Enabled' : 'Disabled'}`); ``` ### Best Practices for New Architecture 1. **Enable on pilot screens first, measure, then expand** 2. **Convert high-traffic NativeModules first** 3. **Use Codegen for type-safe bindings** 4. **Pair Fabric with list virtualization and memoized renderers** 5. **Run `npx expo-doctor` to check library compatibility** ## Expo Best Practices (SDK 52+) ### Expo vs Bare Workflow Decision ```tsx // Use Expo (managed workflow) when: // - Building standard features (camera, location, notifications) // - Need rapid iteration with EAS Build // - Team has limited native development experience // - Project doesn't need custom native modules // Use Bare Workflow when: // - Need custom native modules not available in Expo // - Require specific native SDK integrations // - Need fine-grained control over native configuration ``` ### Continuous Native Generation (CNG) ```bash # Delete native directories before upgrading rm -rf android ios # Regenerate with prebuild npx expo prebuild # Or let EAS Build handle it eas build --platform all ``` ### Expo Router for Navigation ```tsx // app/_layout.tsx import { Stack } from 'expo-router'; export default function RootLayout() { return ( ); } // app/(tabs)/index.tsx export default function HomeScreen() { return ( Go to Profile View Details ); } // app/details/[id].tsx import { useLocalSearchParams } from 'expo-router'; export default function DetailsScreen() { const { id } = useLocalSearchParams<{ id: string }>(); return Details for {id}; } ``` ### Expo Fetch (WinterCG-compliant) ```tsx // Modern streaming fetch for AI APIs import { fetch } from 'expo/fetch'; async function streamResponse(prompt: string) { const response = await fetch('https://api.ai-service.com/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt }), }); const reader = response.body?.getReader(); while (true) { const { done, value } = await reader!.read(); if (done) break; const text = new TextDecoder().decode(value); console.log(text); } } ``` ## Performance Optimization ### FlatList Optimization ```tsx // BAD: Unoptimized FlatList function BadList({ data }) { return ( handlePress(item.id)} />} keyExtractor={(item, index) => index.toString()} // Array index as key! /> ); } // GOOD: Optimized FlatList const ITEM_HEIGHT = 72; function OptimizedList({ data, onItemPress }) { const renderItem = useCallback( ({ item }: { item: DataItem }) => ( ), [onItemPress] ); const keyExtractor = useCallback((item: DataItem) => item.id, []); const getItemLayout = useCallback( (_: unknown, index: number) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index, }), [] ); return ( ); } // Memoized list item const MemoizedListItem = React.memo( function ListItem({ item, onPress }: ListItemProps) { const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]); return ( {item.title} ); } ); ``` ### FlashList for Better Performance ```tsx // For lists with 1000+ items, use FlashList from Shopify import { FlashList } from '@shopify/flash-list'; function HighPerformanceList({ data }) { return ( } estimatedItemSize={72} keyExtractor={(item) => item.id} /> ); } ``` ### Image Optimization ```tsx // Use expo-image or react-native-fast-image for better caching import { Image } from 'expo-image'; function OptimizedImage({ uri }: { uri: string }) { return ( ); } ``` ### Animation Performance ```tsx // Use Reanimated for 60fps animations on UI thread import Animated, { useSharedValue, useAnimatedStyle, withSpring, withTiming, } from 'react-native-reanimated'; function AnimatedCard() { const scale = useSharedValue(1); const opacity = useSharedValue(1); const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }], opacity: opacity.value, })); const handlePressIn = () => { scale.value = withSpring(0.95); opacity.value = withTiming(0.8); }; const handlePressOut = () => { scale.value = withSpring(1); opacity.value = withTiming(1); }; return ( Press Me ); } ``` ## Navigation Patterns (React Navigation v7) ### Type-Safe Navigation ```tsx // types/navigation.ts import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import type { BottomTabScreenProps } from '@react-navigation/bottom-tabs'; export type RootStackParamList = { Home: undefined; Profile: { userId: string }; Settings: undefined; Details: { itemId: string; title: string }; }; export type TabParamList = { Feed: undefined; Search: { query?: string }; Notifications: undefined; }; export type RootStackScreenProps = NativeStackScreenProps; export type TabScreenProps = BottomTabScreenProps; // Usage in screen function ProfileScreen({ route, navigation }: RootStackScreenProps<'Profile'>) { const { userId } = route.params; // Type-safe! const goToDetails = () => { navigation.navigate('Details', { itemId: '123', title: 'Test' }); // Type-checked! }; } ``` ### Custom Navigation Hooks ```tsx // hooks/useTypedNavigation.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 } from '../types/navigation'; export function useAppNavigation() { return useNavigation>(); } export function useAppRoute() { return useRoute>(); } // Usage function MyComponent() { const navigation = useAppNavigation(); const route = useAppRoute<'Profile'>(); navigation.navigate('Details', { itemId: '123', title: 'Test' }); } ``` ### Deep Linking Configuration ```tsx // navigation/linking.ts import { LinkingOptions } from '@react-navigation/native'; import * as Linking from 'expo-linking'; const linking: LinkingOptions = { prefixes: [Linking.createURL('/'), 'myapp://'], config: { screens: { Home: '', Profile: 'profile/:userId', Details: 'details/:itemId', Settings: 'settings', }, }, }; // App.tsx function App() { return ( {/* screens */} ); } ``` ## Platform-Specific Code ### File-Based Platform Extensions ```tsx // components/Button/Button.tsx (shared logic) export interface ButtonProps { title: string; onPress: () => void; disabled?: boolean; } // components/Button/Button.ios.tsx import { TouchableOpacity, Text } from 'react-native'; import type { ButtonProps } from './Button'; export function Button({ title, onPress, disabled }: ButtonProps) { return ( {title} ); } // components/Button/Button.android.tsx import { TouchableNativeFeedback, Text, View } from 'react-native'; import type { ButtonProps } from './Button'; export function Button({ title, onPress, disabled }: ButtonProps) { return ( {title} ); } ``` ### Platform-Specific Hooks ```tsx // hooks/useHaptics.ts import { Platform } from 'react-native'; import * as Haptics from 'expo-haptics'; export function useHaptics() { const light = async () => { if (Platform.OS === 'ios') { await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } else { await Haptics.selectionAsync(); } }; const success = async () => { await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); }; const error = async () => { await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); }; return { light, success, error }; } ``` ## State Management ### Zustand for React Native ```tsx // stores/authStore.ts import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import AsyncStorage from '@react-native-async-storage/async-storage'; interface AuthState { user: User | null; token: string | null; isLoading: boolean; login: (email: string, password: string) => Promise; logout: () => void; hydrated: boolean; setHydrated: (hydrated: boolean) => void; } export const useAuthStore = create()( persist( (set, get) => ({ user: null, token: null, isLoading: false, hydrated: false, setHydrated: (hydrated) => set({ hydrated }), login: async (email, password) => { set({ isLoading: true }); try { const { user, token } = await authApi.login(email, password); set({ user, token, isLoading: false }); } catch (error) { set({ isLoading: false }); throw error; } }, logout: () => { set({ user: null, token: null }); }, }), { name: 'auth-storage', storage: createJSONStorage(() => AsyncStorage), onRehydrateStorage: () => (state) => { state?.setHydrated(true); }, } ) ); // Usage with hydration check function AuthGuard({ children }) { const { hydrated, user } = useAuthStore(); if (!hydrated) { return ; } if (!user) { return ; } return children; } ``` ### TanStack Query for Server State ```tsx // hooks/useProducts.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; export function useProducts(categoryId?: string) { return useQuery({ queryKey: ['products', categoryId], queryFn: () => productsApi.getAll(categoryId), staleTime: 5 * 60 * 1000, // 5 minutes }); } export function useProduct(id: string) { return useQuery({ queryKey: ['products', id], queryFn: () => productsApi.getById(id), enabled: !!id, }); } export function useCreateProduct() { const queryClient = useQueryClient(); return useMutation({ mutationFn: productsApi.create, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['products'] }); }, }); } // Usage in component function ProductsList({ categoryId }: { categoryId: string }) { const { data: products, isLoading, error, refetch } = useProducts(categoryId); if (isLoading) return ; if (error) return ; return ( } keyExtractor={(item) => item.id} onRefresh={refetch} refreshing={isLoading} /> ); } ``` ## Styling Patterns ### StyleSheet Best Practices ```tsx // BAD: Inline styles function BadComponent() { return ( Title ); } // GOOD: StyleSheet.create function GoodComponent() { return ( Title ); } const styles = StyleSheet.create({ container: { flex: 1, padding: 16, backgroundColor: '#fff', }, title: { fontSize: 18, fontWeight: 'bold', color: '#333', }, }); ``` ### Design Tokens and Theme ```tsx // theme/tokens.ts export const colors = { primary: { 50: '#eff6ff', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', }, neutral: { 50: '#fafafa', 100: '#f5f5f5', 900: '#171717', }, error: '#ef4444', success: '#22c55e', } as const; export const spacing = { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, } as const; export const typography = { h1: { fontSize: 32, fontWeight: '700' as const, lineHeight: 40 }, h2: { fontSize: 24, fontWeight: '600' as const, lineHeight: 32 }, body: { fontSize: 16, fontWeight: '400' as const, lineHeight: 24 }, caption: { fontSize: 12, fontWeight: '400' as const, lineHeight: 16 }, } as const; // theme/ThemeContext.tsx import { createContext, useContext } from 'react'; import { useColorScheme } from 'react-native'; const ThemeContext = createContext(lightTheme); export function ThemeProvider({ children }: { children: React.ReactNode }) { const colorScheme = useColorScheme(); const theme = colorScheme === 'dark' ? darkTheme : lightTheme; return ( {children} ); } export const useTheme = () => useContext(ThemeContext); ``` ### Styled Components / NativeWind ```tsx // Using NativeWind (Tailwind for React Native) import { styled } from 'nativewind'; const StyledView = styled(View); const StyledText = styled(Text); function Card({ title, description }: CardProps) { return ( {title} {description} ); } ``` ## Feature-Based Folder Structure ``` src/ app/ # Expo Router pages (if using Expo Router) (tabs)/ index.tsx profile.tsx _layout.tsx features/ # Feature modules auth/ components/ LoginForm.tsx RegisterForm.tsx SocialLoginButtons.tsx hooks/ useAuth.ts useSession.ts screens/ LoginScreen.tsx RegisterScreen.tsx services/ authApi.ts stores/ authStore.ts types/ auth.types.ts index.ts # Public exports products/ components/ ProductCard.tsx ProductList.tsx ProductFilters.tsx hooks/ useProducts.ts useProductSearch.ts screens/ ProductListScreen.tsx ProductDetailScreen.tsx services/ productsApi.ts types/ products.types.ts index.ts shared/ # Shared across features components/ Button/ Button.tsx Button.ios.tsx Button.android.tsx Button.test.tsx Input/ Card/ hooks/ useDebounce.ts useNetwork.ts useHaptics.ts utils/ formatting.ts validation.ts services/ apiClient.ts storage.ts theme/ tokens.ts ThemeContext.tsx types/ common.ts navigation.ts assets/ images/ fonts/ config/ env.ts constants.ts ``` ## TypeScript Best Practices ### Component Props Typing ```tsx // Explicit prop types with JSDoc interface CardProps { /** The card title */ title: string; /** Optional subtitle */ subtitle?: string; /** Card content */ children: React.ReactNode; /** Callback when card is pressed */ onPress?: () => void; /** Visual variant */ variant?: 'default' | 'outlined' | 'elevated'; /** Test ID for E2E testing */ testID?: string; } function Card({ title, subtitle, children, onPress, variant = 'default', testID, }: CardProps) { const Wrapper = onPress ? TouchableOpacity : View; return ( {title} {subtitle && {subtitle}} {children} ); } ``` ### Generic Components ```tsx interface ListProps { items: T[]; renderItem: (item: T, index: number) => React.ReactNode; keyExtractor: (item: T) => string; emptyComponent?: React.ReactNode; ListHeaderComponent?: React.ComponentType | React.ReactElement; } function GenericList({ items, renderItem, keyExtractor, emptyComponent, ListHeaderComponent, }: ListProps) { if (items.length === 0 && emptyComponent) { return <>{emptyComponent}; } return ( renderItem(item, index)} keyExtractor={keyExtractor} ListHeaderComponent={ListHeaderComponent} /> ); } // Usage with type inference } keyExtractor={(user) => user.id} emptyComponent={} /> ``` ### Native Module Types ```tsx // types/native.d.ts declare module 'react-native' { interface NativeModulesStatic { CustomModule: { getValue(): Promise; setValue(value: string): Promise; addListener(event: string): void; removeListeners(count: number): void; }; } } // Usage import { NativeModules, NativeEventEmitter } from 'react-native'; const { CustomModule } = NativeModules; const eventEmitter = new NativeEventEmitter(CustomModule); function useCustomModule() { useEffect(() => { const subscription = eventEmitter.addListener('onValueChange', (event) => { console.log(event); }); return () => subscription.remove(); }, []); return { getValue: CustomModule.getValue, setValue: CustomModule.setValue, }; } ``` ## Anti-Patterns to Refactor ### 1. FlatList Performance Issues ```tsx // BAD: Inline functions and missing optimizations ( handlePress(item.id)}> {item.name} )} keyExtractor={(item, index) => index.toString()} /> // GOOD: Memoized components and proper configuration const ItemComponent = React.memo(({ item, onPress }) => ( onPress(item.id)}> {item.name} )); const renderItem = useCallback(({ item }) => ( ), [handlePress]); item.id, [])} getItemLayout={useCallback((_, index) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index, }), [])} /> ``` ### 2. Inline Styles ```tsx // BAD: Inline styles create new objects every render Hello // GOOD: StyleSheet + array syntax for dynamic styles Hello const styles = StyleSheet.create({ container: { flex: 1, padding: 16 }, text: { fontSize: 18, color: 'black' }, textPressed: { color: 'blue' }, }); ``` ### 3. API Calls in Components ```tsx // BAD: API logic mixed with component function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch(`/api/users/${userId}`) .then(res => res.json()) .then(setUser) .finally(() => setLoading(false)); }, [userId]); // ... render logic } // GOOD: Extract to custom hook or use TanStack Query function useUser(userId: string) { return useQuery({ queryKey: ['users', userId], queryFn: () => userApi.getById(userId), enabled: !!userId, }); } function UserProfile({ userId }) { const { data: user, isLoading, error } = useUser(userId); if (isLoading) return ; if (error) return ; return ; } ``` ### 4. Missing Error Boundaries ```tsx // BAD: No error handling function App() { return ( ); } // GOOD: Error boundary with fallback import { ErrorBoundary } from 'react-error-boundary'; function ErrorFallback({ error, resetErrorBoundary }) { return ( Something went wrong {error.message}