npx skills add https://github.com/wshobson/agents --skill react-native-architectureHow React Native Architecture fits into a Paperclip company.
React Native Architecture drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.
Pre-configured AI company — 18 agents, 18 skills, one-time purchase.
SKILL.md666 linesExpandCollapse
---name: react-native-architecturedescription: Build production React Native apps with Expo, navigation, native modules, offline sync, and cross-platform patterns. Use when developing mobile apps, implementing native integrations, or architecting React Native projects.--- # React Native Architecture Production-ready patterns for React Native development with Expo, including navigation, state management, native modules, and offline-first architecture. ## When to Use This Skill - Starting a new React Native or Expo project- Implementing complex navigation patterns- Integrating native modules and platform APIs- Building offline-first mobile applications- Optimizing React Native performance- Setting up CI/CD for mobile releases ## Core Concepts ### 1. Project Structure ```src/├── app/ # Expo Router screens│ ├── (auth)/ # Auth group│ ├── (tabs)/ # Tab navigation│ └── _layout.tsx # Root layout├── components/│ ├── ui/ # Reusable UI components│ └── features/ # Feature-specific components├── hooks/ # Custom hooks├── services/ # API and native services├── stores/ # State management├── utils/ # Utilities└── types/ # TypeScript types``` ### 2. Expo vs Bare React Native | Feature | Expo | Bare RN || ------------------ | -------------- | -------------- || Setup complexity | Low | High || Native modules | EAS Build | Manual linking || OTA updates | Built-in | Manual setup || Build service | EAS | Custom CI || Custom native code | Config plugins | Direct access | ## Quick Start ```bash# Create new Expo projectnpx create-expo-app@latest my-app -t expo-template-blank-typescript # Install essential dependenciesnpx expo install expo-router expo-status-bar react-native-safe-area-contextnpx expo install @react-native-async-storage/async-storagenpx expo install expo-secure-store expo-haptics``` ```typescript// app/_layout.tsximport { Stack } from 'expo-router'import { ThemeProvider } from '@/providers/ThemeProvider'import { QueryProvider } from '@/providers/QueryProvider' export default function RootLayout() { return ( <QueryProvider> <ThemeProvider> <Stack screenOptions={{ headerShown: false }}> <Stack.Screen name="(tabs)" /> <Stack.Screen name="(auth)" /> <Stack.Screen name="modal" options={{ presentation: 'modal' }} /> </Stack> </ThemeProvider> </QueryProvider> )}``` ## Patterns ### Pattern 1: Expo Router Navigation ```typescript// app/(tabs)/_layout.tsximport { Tabs } from 'expo-router'import { Home, Search, User, Settings } from 'lucide-react-native'import { useTheme } from '@/hooks/useTheme' export default function TabLayout() { const { colors } = useTheme() return ( <Tabs screenOptions={{ tabBarActiveTintColor: colors.primary, tabBarInactiveTintColor: colors.textMuted, tabBarStyle: { backgroundColor: colors.background }, headerShown: false, }} > <Tabs.Screen name="index" options={{ title: 'Home', tabBarIcon: ({ color, size }) => <Home size={size} color={color} />, }} /> <Tabs.Screen name="search" options={{ title: 'Search', tabBarIcon: ({ color, size }) => <Search size={size} color={color} />, }} /> <Tabs.Screen name="profile" options={{ title: 'Profile', tabBarIcon: ({ color, size }) => <User size={size} color={color} />, }} /> <Tabs.Screen name="settings" options={{ title: 'Settings', tabBarIcon: ({ color, size }) => <Settings size={size} color={color} />, }} /> </Tabs> )} // app/(tabs)/profile/[id].tsx - Dynamic routeimport { useLocalSearchParams } from 'expo-router' export default function ProfileScreen() { const { id } = useLocalSearchParams<{ id: string }>() return <UserProfile userId={id} />} // Navigation from anywhereimport { router } from 'expo-router' // Programmatic navigationrouter.push('/profile/123')router.replace('/login')router.back() // With paramsrouter.push({ pathname: '/product/[id]', params: { id: '123', referrer: 'home' },})``` ### Pattern 2: Authentication Flow ```typescript// providers/AuthProvider.tsximport { createContext, useContext, useEffect, useState } from 'react'import { useRouter, useSegments } from 'expo-router'import * as SecureStore from 'expo-secure-store' interface AuthContextType { user: User | null isLoading: boolean signIn: (credentials: Credentials) => Promise<void> signOut: () => Promise<void>} const AuthContext = createContext<AuthContextType | null>(null) export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null) const [isLoading, setIsLoading] = useState(true) const segments = useSegments() const router = useRouter() // Check authentication on mount useEffect(() => { checkAuth() }, []) // Protect routes useEffect(() => { if (isLoading) return const inAuthGroup = segments[0] === '(auth)' if (!user && !inAuthGroup) { router.replace('/login') } else if (user && inAuthGroup) { router.replace('/(tabs)') } }, [user, segments, isLoading]) async function checkAuth() { try { const token = await SecureStore.getItemAsync('authToken') if (token) { const userData = await api.getUser(token) setUser(userData) } } catch (error) { await SecureStore.deleteItemAsync('authToken') } finally { setIsLoading(false) } } async function signIn(credentials: Credentials) { const { token, user } = await api.login(credentials) await SecureStore.setItemAsync('authToken', token) setUser(user) } async function signOut() { await SecureStore.deleteItemAsync('authToken') setUser(null) } if (isLoading) { return <SplashScreen /> } return ( <AuthContext.Provider value={{ user, isLoading, signIn, signOut }}> {children} </AuthContext.Provider> )} export const useAuth = () => { const context = useContext(AuthContext) if (!context) throw new Error('useAuth must be used within AuthProvider') return context}``` ### Pattern 3: Offline-First with React Query ```typescript// providers/QueryProvider.tsximport { QueryClient, QueryClientProvider } from '@tanstack/react-query'import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'import AsyncStorage from '@react-native-async-storage/async-storage'import NetInfo from '@react-native-community/netinfo'import { onlineManager } from '@tanstack/react-query' // Sync online statusonlineManager.setEventListener((setOnline) => { return NetInfo.addEventListener((state) => { setOnline(!!state.isConnected) })}) const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours staleTime: 1000 * 60 * 5, // 5 minutes retry: 2, networkMode: 'offlineFirst', }, mutations: { networkMode: 'offlineFirst', }, },}) const asyncStoragePersister = createAsyncStoragePersister({ storage: AsyncStorage, key: 'REACT_QUERY_OFFLINE_CACHE',}) export function QueryProvider({ children }: { children: React.ReactNode }) { return ( <PersistQueryClientProvider client={queryClient} persistOptions={{ persister: asyncStoragePersister }} > {children} </PersistQueryClientProvider> )} // hooks/useProducts.tsimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' export function useProducts() { return useQuery({ queryKey: ['products'], queryFn: api.getProducts, // Use stale data while revalidating placeholderData: (previousData) => previousData, })} export function useCreateProduct() { const queryClient = useQueryClient() return useMutation({ mutationFn: api.createProduct, // Optimistic update onMutate: async (newProduct) => { await queryClient.cancelQueries({ queryKey: ['products'] }) const previous = queryClient.getQueryData(['products']) queryClient.setQueryData(['products'], (old: Product[]) => [ ...old, { ...newProduct, id: 'temp-' + Date.now() }, ]) return { previous } }, onError: (err, newProduct, context) => { queryClient.setQueryData(['products'], context?.previous) }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['products'] }) }, })}``` ### Pattern 4: Native Module Integration ```typescript// services/haptics.tsimport * as Haptics from "expo-haptics";import { Platform } from "react-native"; export const haptics = { light: () => { if (Platform.OS !== "web") { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } }, medium: () => { if (Platform.OS !== "web") { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); } }, heavy: () => { if (Platform.OS !== "web") { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy); } }, success: () => { if (Platform.OS !== "web") { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); } }, error: () => { if (Platform.OS !== "web") { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); } },}; // services/biometrics.tsimport * as LocalAuthentication from "expo-local-authentication"; export async function authenticateWithBiometrics(): Promise<boolean> { const hasHardware = await LocalAuthentication.hasHardwareAsync(); if (!hasHardware) return false; const isEnrolled = await LocalAuthentication.isEnrolledAsync(); if (!isEnrolled) return false; const result = await LocalAuthentication.authenticateAsync({ promptMessage: "Authenticate to continue", fallbackLabel: "Use passcode", disableDeviceFallback: false, }); return result.success;} // services/notifications.tsimport * as Notifications from "expo-notifications";import { Platform } from "react-native";import Constants from "expo-constants"; Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: true, }),}); export async function registerForPushNotifications() { let token: string | undefined; if (Platform.OS === "android") { await Notifications.setNotificationChannelAsync("default", { name: "default", importance: Notifications.AndroidImportance.MAX, vibrationPattern: [0, 250, 250, 250], }); } const { status: existingStatus } = await Notifications.getPermissionsAsync(); let finalStatus = existingStatus; if (existingStatus !== "granted") { const { status } = await Notifications.requestPermissionsAsync(); finalStatus = status; } if (finalStatus !== "granted") { return null; } const projectId = Constants.expoConfig?.extra?.eas?.projectId; token = (await Notifications.getExpoPushTokenAsync({ projectId })).data; return token;}``` ### Pattern 5: Platform-Specific Code ```typescript// components/ui/Button.tsximport { Platform, Pressable, StyleSheet, Text, ViewStyle } from 'react-native'import * as Haptics from 'expo-haptics'import Animated, { useAnimatedStyle, useSharedValue, withSpring,} from 'react-native-reanimated' const AnimatedPressable = Animated.createAnimatedComponent(Pressable) interface ButtonProps { title: string onPress: () => void variant?: 'primary' | 'secondary' | 'outline' disabled?: boolean} export function Button({ title, onPress, variant = 'primary', disabled = false,}: ButtonProps) { const scale = useSharedValue(1) const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }], })) const handlePressIn = () => { scale.value = withSpring(0.95) if (Platform.OS !== 'web') { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) } } const handlePressOut = () => { scale.value = withSpring(1) } return ( <AnimatedPressable onPress={onPress} onPressIn={handlePressIn} onPressOut={handlePressOut} disabled={disabled} style={[ styles.button, styles[variant], disabled && styles.disabled, animatedStyle, ]} > <Text style={[styles.text, styles[`${variant}Text`]]}>{title}</Text> </AnimatedPressable> )} // Platform-specific files// Button.ios.tsx - iOS-specific implementation// Button.android.tsx - Android-specific implementation// Button.web.tsx - Web-specific implementation // Or use Platform.selectconst styles = StyleSheet.create({ button: { paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8, alignItems: 'center', ...Platform.select({ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, }, android: { elevation: 4, }, }), }, primary: { backgroundColor: '#007AFF', }, secondary: { backgroundColor: '#5856D6', }, outline: { backgroundColor: 'transparent', borderWidth: 1, borderColor: '#007AFF', }, disabled: { opacity: 0.5, }, text: { fontSize: 16, fontWeight: '600', }, primaryText: { color: '#FFFFFF', }, secondaryText: { color: '#FFFFFF', }, outlineText: { color: '#007AFF', },})``` ### Pattern 6: Performance Optimization ```typescript// components/ProductList.tsximport { FlashList } from '@shopify/flash-list'import { memo, useCallback } from 'react' interface ProductListProps { products: Product[] onProductPress: (id: string) => void} // Memoize list itemconst ProductItem = memo(function ProductItem({ item, onPress,}: { item: Product onPress: (id: string) => void}) { const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]) return ( <Pressable onPress={handlePress} style={styles.item}> <FastImage source={{ uri: item.image }} style={styles.image} resizeMode="cover" /> <Text style={styles.title}>{item.name}</Text> <Text style={styles.price}>${item.price}</Text> </Pressable> )}) export function ProductList({ products, onProductPress }: ProductListProps) { const renderItem = useCallback( ({ item }: { item: Product }) => ( <ProductItem item={item} onPress={onProductPress} /> ), [onProductPress] ) const keyExtractor = useCallback((item: Product) => item.id, []) return ( <FlashList data={products} renderItem={renderItem} keyExtractor={keyExtractor} estimatedItemSize={100} // Performance optimizations removeClippedSubviews={true} maxToRenderPerBatch={10} windowSize={5} // Pull to refresh onRefresh={onRefresh} refreshing={isRefreshing} /> )}``` ## EAS Build & Submit ```json// eas.json{ "cli": { "version": ">= 5.0.0" }, "build": { "development": { "developmentClient": true, "distribution": "internal", "ios": { "simulator": true } }, "preview": { "distribution": "internal", "android": { "buildType": "apk" } }, "production": { "autoIncrement": true } }, "submit": { "production": { "ios": { "appleId": "your@email.com", "ascAppId": "123456789" }, "android": { "serviceAccountKeyPath": "./google-services.json" } } }}``` ```bash# Build commandseas build --platform ios --profile developmenteas build --platform android --profile previeweas build --platform all --profile production # Submit to storeseas submit --platform ioseas submit --platform android # OTA updateseas update --branch production --message "Bug fixes"``` ## Best Practices ### Do's - **Use Expo** - Faster development, OTA updates, managed native code- **FlashList over FlatList** - Better performance for long lists- **Memoize components** - Prevent unnecessary re-renders- **Use Reanimated** - 60fps animations on native thread- **Test on real devices** - Simulators miss real-world issues ### Don'ts - **Don't inline styles** - Use StyleSheet.create for performance- **Don't fetch in render** - Use useEffect or React Query- **Don't ignore platform differences** - Test on both iOS and Android- **Don't store secrets in code** - Use environment variables- **Don't skip error boundaries** - Mobile crashes are unforgivingAccessibility Compliance
This walks you through implementing proper WCAG 2.2 compliance with real code patterns for screen readers, keyboard navigation, and mobile accessibility. It cov
Airflow Dag Patterns
If you're building data pipelines with Airflow, this skill gives you production-ready DAG patterns that actually work in the real world. It covers TaskFlow API
Angular Migration
Migrating from AngularJS to Angular is notoriously painful, and this skill tackles the practical stuff that makes or breaks these projects. It covers hybrid app