React Native Conventions
Component Architecture
- Functional components with hooks — no class components
- One component per file, file name matches component name (PascalCase)
- Component structure: types/interfaces → component → styles → export
- Keep components under 200 lines — extract sub-components when needed
- Use
React.memo()for list items and expensive renders
State Management (Zustand)
- Use Zustand for all shared/global state — no Redux, no Context for state
- One store per domain:
useAuthStore,useLocationStore,useCartStore - Keep stores flat — avoid deeply nested state
- Use selectors to prevent unnecessary re-renders:
const name = useUserStore(s => s.name) - Separate actions from state:
interface AuthStore { user: User | null; token: string | null; // Actions login: (credentials: Credentials) => Promise<void>; logout: () => void; } - Use
persistmiddleware with MMKV for offline-capable stores - Never put server-fetched data in Zustand — that belongs in TanStack Query
Data Fetching (TanStack Query / React Query)
- Use TanStack Query for ALL server state — never fetch in useEffect
- Define query keys consistently:
['users', userId],['posts', { page, filter }] - Extract custom hooks:
useUser(id),usePosts(filter) - Configure stale times per query type:
- User profile: 5 minutes
- Lists: 1 minute
- Real-time data: 0 (always refetch)
- Use
useMutationfor all write operations withonSuccessinvalidation - Use
useInfiniteQueryfor paginated lists - Configure
retry: 3for network resilience on mobile - Example:
const useUser = (id: string) => useQuery({ queryKey: ['users', id], queryFn: () => api.getUser(id), staleTime: 5 * 60 * 1000, });
Real-time (Centrifugal / Centrifugo)
- Use Centrifugo client SDK for real-time subscriptions
- One
Centrifugeclient for the app, created once at module scope — not per screen getSubscription(channel) ?? newSubscription(channel)—newSubscriptionthrows if the channel is already in the client's registry, which is exactly what a remounting screen does- Subscribe in custom hooks:
useChannel('chat:${roomId}') - Clean up on unmount by removing your handler —
unsubscribe()does not remove listeners, so remounts stack handlers and each message is processed N times - An event updates the TanStack Query cache; it never becomes a second copy of the data.
Full entity on the wire →
setQueryData; partial or "something changed" →invalidateQueries - Invalidate on reconnect — the socket does not backfill what it missed while down
- Mint the connection token on the Rails side and wire
getToken(not a statictoken), or the socket dies silently when it expires - Use presence channels for online status — presence is ephemeral client state, not Query state
- Deep guide →
references/realtime-centrifugo.md
Navigation
- Use React Navigation (or Expo Router if using Expo)
- Type-safe navigation with typed param lists
- Deep linking configuration for push notifications
- Keep navigation structure flat — max 3 levels of nesting
Styling
- Use StyleSheet.create for all styles — no inline style objects
- Design tokens: define colors, spacing, typography in a theme file
- Responsive: use
DimensionsoruseWindowDimensionsfor adaptive layouts - Support dark mode via theme context
Performance
- Use
FlatList(neverScrollView) for lists > 20 items - Set
keyExtractorandgetItemLayoutfor FlatList optimization - Use
react-native-fast-imagefor cached image loading - Avoid anonymous functions in render — use
useCallback - Profile with Flipper and React DevTools
Offline Support
- Persist critical Zustand stores with MMKV
- TanStack Query
persisterfor caching API responses offline - Feed
onlineManagerfrom NetInfo — Query cannot tell a dead radio from a slow server - Queue mutations when offline, replay on reconnect. Register
queryClient.setMutationDefaultsat app/module scope, not in the screen: only mutation state is persisted (functions are not serializable), so after an app kill the resumed mutation has nomutationFnand dies withNo mutationFn found— the user saw a success toast an hour ago - A retried mutation must be idempotent — generate the
Idempotency-Keyonce at the call site so every replay carries the same one; "replayed on reconnect" means the server may see it twice - Show clear offline indicators — a queued write must not look identical to a sent one
- Deep guide →
references/offline-and-mutations.md
Libraries — Prefer Community Packages
- Navigation:
@react-navigation/native - Storage:
react-native-mmkv(faster than AsyncStorage) - Images:
react-native-fast-image - Maps:
react-native-maps(with PostGIS backend) - Push notifications:
@react-native-firebase/messaging - Forms:
react-hook-formwithzodvalidation - Animations:
react-native-reanimated - Gestures:
react-native-gesture-handler - Icons:
react-native-vector-iconsorexpo-icons - HTTP:
axioswith interceptors for auth tokens - Date/time:
date-fns(tree-shakeable) overmoment - Geolocation:
react-native-geolocation-service
TypeScript
- Strict mode enabled — no
anytypes - Define interfaces for all API responses, props, and store state
- Use discriminated unions for state machines (loading | success | error)
Deep guides (read on demand, do not preload)
- NetInfo →
onlineManager, persisting the cache to MMKV,setMutationDefaultsat app scope, idempotency keys that survive a cold start, optimistic updates with rollback →references/offline-and-mutations.md - One Centrifuge client, the
getSubscription ?? newSubscriptionrule, handler cleanup,setQueryDatavsinvalidateQueries, reconnect backfill, presence →references/realtime-centrifugo.md
Related, owned elsewhere — do not duplicate: list/render/animation performance rules live in the
react-native-best-practices skill (38 rule files); axios token-refresh interceptors live in
../react-native-dev/references/react-native-patterns.md.
- Export types alongside components