| name | react-native-dev |
| description | Build React Native mobile features with Zustand state management, TanStack Query data fetching, and Centrifugo real-time subscriptions. Use this skill whenever someone asks to build a mobile screen, create a component, add a Zustand store, write a TanStack Query hook, or says things like "build the mobile UI for X", "create a screen for Y", "add real-time updates", "implement offline support", "set up navigation for X", or "how should I structure this component". Also trigger when someone mentions React Native performance, MMKV storage, map integration, or push notification implementation. |
| model | sonnet |
| allowed-tools | Read, Grep, Glob, Write, Edit |
React Native Developer
Purpose
Build mobile features using our React Native stack: Zustand for state, TanStack Query for server data, Centrifugo for real-time, and community libraries over custom code.
Development Protocol
1. Understand the Feature
- Which screens are involved?
- What data comes from the API vs local state?
- Is real-time needed? (Centrifugo subscription)
- Does it need offline support? (Zustand persist + TanStack cache)
2. API Layer (TanStack Query)
- Define query keys:
['orders', orderId], ['orders', { status, page }]
- Create custom hooks in
mobile/src/hooks/queries/:
export const useOrders = (filter: OrderFilter) =>
useQuery({
queryKey: ['orders', filter],
queryFn: () => api.orders.list(filter),
staleTime: 60_000,
});
export const useCreateOrder = () =>
useMutation({
mutationFn: api.orders.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['orders'] });
},
});
3. State Management (Zustand)
- Only for CLIENT state — auth, UI preferences, offline queues, navigation state
- Never duplicate server data in Zustand (that's TanStack Query's job)
- Create stores in
mobile/src/stores/:
export const useAuthStore = create<AuthStore>()(
persist(
(set) => ({
token: null,
user: null,
login: async (credentials) => { },
logout: () => set({ token: null, user: null }),
}),
{ name: 'auth-store', storage: createMMKVStorage() }
)
);
4. Component Structure
mobile/src/
├── screens/ # Full screen components (connected to navigation)
├── components/
│ ├── common/ # Reusable: Button, Input, Card, Avatar
│ └── features/ # Feature-specific: OrderCard, MapMarker
├── hooks/
│ ├── queries/ # TanStack Query hooks
│ ├── mutations/ # TanStack Mutation hooks
│ └── realtime/ # Centrifugo subscription hooks
├── stores/ # Zustand stores
├── services/ # API client, Centrifugo client
├── navigation/ # React Navigation config
├── theme/ # Colors, spacing, typography
└── utils/ # Helpers, formatters
5. Real-time (Centrifugo)
Owned by std-react-native → @skills/std-react-native/references/realtime-centrifugo.md, which
is scoped to React Native work. This skill previously carried its own hook here; it called
centrifuge.subscribe(channel), which is not this client's API — subscriptions are created
with newSubscription() and retrieved with getSubscription() — so the snippet could not run,
and the shape it taught had the exact leak the reference exists to prevent.
Load-bearing rules, because they change what you write:
- One
Centrifuge client for the app, created once. Not per screen, not per hook.
newSubscription() throws if the channel already has a subscription — and a remounting
screen calls it twice. Always centrifuge.getSubscription(ch) ?? centrifuge.newSubscription(ch).
- A real-time event updates the Query cache; it never becomes a second copy of the data.
TanStack Query owns server state, Zustand owns client state — a socket quietly offers a third,
and taking it means the same order exists twice with different values. Nothing crashes; the app
is just wrong sometimes.
- Unsubscribing does not remove handlers you already set. Remove your handler on cleanup,
or every remount stacks another and the cache update runs N times per message. Leave the
subscription alive if another screen shares the channel; reap it (
unsubscribe() +
centrifuge.removeSubscription(sub)) only when nothing is listening.
- The connection token expires — wire
getToken, or the socket dies silently after an hour.
What to do with an incoming event:
| The event… | Do |
|---|
| Carries the full updated entity | setQueryData(key, entity) — free update, no refetch |
| Carries only an id / "something changed" | invalidateQueries({ queryKey }) — let Query refetch the truth |
| Is high-frequency (typing, cursors, presence) | Zustand or local state — ephemeral, never server state |
The complete useChannel hook, with the registry and handler-cleanup handling, is in the
reference. Copy it from there rather than reconstructing it — the four lines that look optional
are the ones that break on navigation.
6. Maps & Geospatial (PostGIS Backend)
- Use
react-native-maps with markers from PostGIS spatial queries
- Debounce map region changes before API calls
- Cluster markers for performance:
react-native-map-clustering
- Send viewport bounds to API for efficient spatial queries
Key Libraries Reference
| Need | Library | Notes |
|---|
| State | zustand + react-native-mmkv | Persist with MMKV |
| Server data | @tanstack/react-query | Never useEffect for fetching |
| Forms | react-hook-form + zod | Schema validation |
| Navigation | @react-navigation/native | Type-safe params |
| Maps | react-native-maps | + clustering |
| Images | react-native-fast-image | Cached loading |
| Animations | react-native-reanimated | 60fps |
| HTTP | axios | Interceptors for auth |
| Storage | react-native-mmkv | Fastest local storage |
| Dates | date-fns | Tree-shakeable |
| Push | @react-native-firebase/messaging | FCM |
| Location | react-native-geolocation-service | Background tracking |
See references/react-native-patterns.md for component and hook patterns.
Deep guides (read on demand, do not preload)
- Worked screens, hooks, stores, and navigation for this stack →
references/react-native-patterns.md
Owned by std-react-native (scoped to React Native work)
Decision-shaped, with the bad/good pairs. The file above shows a pattern; these say which and why:
- Real-time (Centrifugo) — one socket, one cache, no second source of truth →
@skills/std-react-native/references/realtime-centrifugo.md
- Offline & mutations — the queue that survives a cold start →
@skills/std-react-native/references/offline-and-mutations.md
Two facts from those that decide what you write, rather than how:
staleTime is per query, not one default. users at 5 minutes and orders at 1 minute is
correct — the table in @skills/std-reactjs/references/data-fetching.md is the reasoning.
- Testing React Native is Jest, not Vitest. There is no DOM, and Metro's transform pipeline is
Jest-based →
@skills/std-testing/references/react-native.md.