React Native Skill
Load with: base.md + typescript.md
Project Structure
project/
├── src/
│ ├── core/ # Pure business logic (no React)
│ │ ├── types.ts
│ │ └── services/
│ ├── components/ # Reusable UI components
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.test.tsx
│ │ │ └── index.ts
│ │ └── index.ts # Barrel export
│ ├── screens/ # Screen components
│ │ ├── Home/
│ │ │ ├── HomeScreen.tsx
│ │ │ ├── useHome.ts # Screen-specific hook
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── navigation/ # Navigation configuration
│ ├── hooks/ # Shared custom hooks
│ ├── store/ # State management
│ └── utils/ # Utilities
├── __tests__/
├── android/
├── ios/
└── CLAUDE.md
Component Patterns
Functional Components Only
typescript1// Good - simple, testable 2interface ButtonProps { 3 label: string; 4 onPress: () => void; 5 disabled?: boolean; 6} 7 8export function Button({ label, onPress, disabled = false }: ButtonProps): JSX.Element { 9 return ( 10 <Pressable onPress={onPress} disabled={disabled}> 11 <Text>{label}</Text> 12 </Pressable> 13 ); 14}
Extract Logic to Hooks
typescript1// useHome.ts - all logic here 2export function useHome() { 3 const [items, setItems] = useState<Item[]>([]); 4 const [loading, setLoading] = useState(false); 5 6 const refresh = useCallback(async () => { 7 setLoading(true); 8 const data = await fetchItems(); 9 setItems(data); 10 setLoading(false); 11 }, []); 12 13 return { items, loading, refresh }; 14} 15 16// HomeScreen.tsx - pure presentation 17export function HomeScreen(): JSX.Element { 18 const { items, loading, refresh } = useHome(); 19 20 return ( 21 <ItemList items={items} loading={loading} onRefresh={refresh} /> 22 ); 23}
Props Interface Always Explicit
typescript1// Always define props interface, even if simple 2interface ItemCardProps { 3 item: Item; 4 onPress: (id: string) => void; 5} 6 7export function ItemCard({ item, onPress }: ItemCardProps): JSX.Element { 8 ... 9}
State Management
Local State First
typescript1// Start with useState, escalate only when needed 2const [value, setValue] = useState('');
Zustand for Global State (if needed)
typescript1// store/useAppStore.ts 2import { create } from 'zustand'; 3 4interface AppState { 5 user: User | null; 6 setUser: (user: User | null) => void; 7} 8 9export const useAppStore = create<AppState>((set) => ({ 10 user: null, 11 setUser: (user) => set({ user }), 12}));
React Query for Server State
typescript1// hooks/useItems.ts 2import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; 3 4export function useItems() { 5 return useQuery({ 6 queryKey: ['items'], 7 queryFn: fetchItems, 8 }); 9} 10 11export function useCreateItem() { 12 const queryClient = useQueryClient(); 13 14 return useMutation({ 15 mutationFn: createItem, 16 onSuccess: () => { 17 queryClient.invalidateQueries({ queryKey: ['items'] }); 18 }, 19 }); 20}
Testing
Component Testing with React Native Testing Library
typescript1import { render, fireEvent } from '@testing-library/react-native'; 2import { Button } from './Button'; 3 4describe('Button', () => { 5 it('calls onPress when pressed', () => { 6 const onPress = jest.fn(); 7 const { getByText } = render(<Button label="Click me" onPress={onPress} />); 8 9 fireEvent.press(getByText('Click me')); 10 11 expect(onPress).toHaveBeenCalledTimes(1); 12 }); 13 14 it('does not call onPress when disabled', () => { 15 const onPress = jest.fn(); 16 const { getByText } = render(<Button label="Click me" onPress={onPress} disabled />); 17 18 fireEvent.press(getByText('Click me')); 19 20 expect(onPress).not.toHaveBeenCalled(); 21 }); 22});
Hook Testing
typescript1import { renderHook, act } from '@testing-library/react-hooks'; 2import { useCounter } from './useCounter'; 3 4describe('useCounter', () => { 5 it('increments counter', () => { 6 const { result } = renderHook(() => useCounter()); 7 8 act(() => { 9 result.current.increment(); 10 }); 11 12 expect(result.current.count).toBe(1); 13 }); 14});
Platform-Specific Code
Use Platform.select Sparingly
typescript1import { Platform } from 'react-native'; 2 3const styles = StyleSheet.create({ 4 shadow: Platform.select({ 5 ios: { 6 shadowColor: '#000', 7 shadowOffset: { width: 0, height: 2 }, 8 shadowOpacity: 0.1, 9 }, 10 android: { 11 elevation: 2, 12 }, 13 }), 14});
Separate Files for Complex Differences
Component/
├── Component.tsx # Shared logic
├── Component.ios.tsx # iOS-specific
├── Component.android.tsx # Android-specific
└── index.ts
React Native Anti-Patterns
- ❌ Inline styles - use StyleSheet.create
- ❌ Logic in render - extract to hooks
- ❌ Deep component nesting - flatten hierarchy
- ❌ Anonymous functions in props - use useCallback
- ❌ Index as key in lists - use stable IDs
- ❌ Direct state mutation - always use setter
- ❌ Mixing business logic with UI - keep core/ pure
- ❌ Ignoring TypeScript errors - fix them
- ❌ Large components - split into smaller pieces