Agent skill

react-native-advanced

React Native and Expo patterns for navigation, data fetching lifecycle, infinite scroll lists, form handling, state persistence, authentication routing, gesture-driven animations, bottom sheets, push notifications, and OTA updates. Use when building Expo/React Native apps that need screen-level data prefetching, auth guards with protected routes, infinite scroll feeds, native form input handling, offline-capable state persistence, platform-specific setup (focus/online managers), fluid animations and gesture interactions, modal bottom sheets, push notification flows, or over-the-air update strategies. Do not use for React web apps.

Stars 3
Forks 0

Install this agent skill to your Project

npx add-skill https://github.com/trancong12102/agentskills/tree/main/react-native-advanced

SKILL.md

React Native Advanced: Expo + TanStack/XState/Zustand Ecosystem

React Native and Expo patterns for apps built with the TanStack ecosystem, XState, and Zustand. This skill extends react-advanced (core cross-platform patterns). Read that skill first for React Query, XState, Zustand, Zod, TanStack Form, and TanStack Table conventions.

Table of Contents

  1. RN Architecture
  2. Required Setup
  3. Data Fetching Without Route Loaders
  4. Navigation + Auth
  5. Lists: FlashList + React Query
  6. TanStack Form in RN
  7. Zustand Persist with MMKV
  8. Animations & Gestures
  9. Bottom Sheets
  10. Push Notifications
  11. OTA Updates
  12. File Organization
  13. Common Pitfalls
  14. Reference Files

RN Architecture

Libraries map differently in React Native compared to web:

Web Library RN Equivalent Key Difference
TanStack Router Expo Router No route loaders on native, file-based navigation
TanStack Start No SSR/server functions on native
TanStack Virtual FlashList Native view recycling, not DOM virtualization
localStorage MMKV Synchronous, native-thread, 30x faster
window events AppState/NetInfo Manual wiring required for focus/online managers
CSS animations Reanimated UI-thread worklets, CSS transitions (v4)
DOM events Gesture Handler Gesture composition API, UI-thread callbacks
<img> expo-image SDWebImage/Glide, blurhash, disk caching
Web Push API expo-notifications FCM/APNs, channels, background tasks
Service Workers expo-updates OTA updates, staged rollout, emergency rollback

Cross-platform libraries (identical API on web and RN): React Query, XState, Zustand, Zod, TanStack Form, TanStack Table


Required Setup

These two integrations are mandatory — without them, React Query's auto-refetch and offline handling do not work in React Native.

focusManager — refetch when app returns to foreground

typescript
// hooks/useAppState.ts
import { useEffect } from "react";
import { AppState, Platform } from "react-native";
import type { AppStateStatus } from "react-native";
import { focusManager } from "@tanstack/react-query";

function onAppStateChange(status: AppStateStatus) {
  if (Platform.OS !== "web") {
    focusManager.setFocused(status === "active");
  }
}

export function useAppState() {
  useEffect(() => {
    const sub = AppState.addEventListener("change", onAppStateChange);
    return () => sub.remove();
  }, []);
}

onlineManager — pause/resume queries based on network

typescript
// hooks/useOnlineManager.ts
import { useEffect } from "react";
import NetInfo from "@react-native-community/netinfo";
import { onlineManager } from "@tanstack/react-query";

export function useOnlineManager() {
  useEffect(() => {
    return NetInfo.addEventListener((state) => {
      onlineManager.setOnline(!!state.isConnected);
    });
  }, []);
}

Call both hooks once in the root layout:

typescript
// app/_layout.tsx
export default function RootLayout() {
  useAppState()
  useOnlineManager()
  return (
    <QueryClientProvider client={queryClient}>
      <Slot />
    </QueryClientProvider>
  )
}

Data Fetching Without Route Loaders

Expo Router has no native route loaders (data loaders are web-only/alpha). The pattern is: prefetch on user interaction, consume in the destination screen.

Prefetch on press (don't await — keep navigation instant)

typescript
function PostListItem({ id }: { id: string }) {
  const queryClient = useQueryClient()
  const router = useRouter()

  return (
    <Pressable
      onPress={() => {
        queryClient.prefetchQuery(postQueryOptions(id)) // fire-and-forget
        router.push(`/posts/${id}`)
      }}
    >
      <Text>{title}</Text>
    </Pressable>
  )
}

Refetch when screen regains focus

Screens stay mounted in a native stack. useEffect does not re-run when navigating back. Use useFocusEffect to invalidate stale data:

typescript
import { useFocusEffect } from "expo-router";

export function useRefreshOnFocus(queryKey: unknown[]) {
  const queryClient = useQueryClient();
  const firstRender = useRef(true);

  useFocusEffect(
    useCallback(() => {
      if (firstRender.current) {
        firstRender.current = false;
        return;
      }
      queryClient.invalidateQueries({ queryKey });
    }, [queryClient, queryKey]),
  );
}

Use invalidateQueries (respects staleTime) instead of refetch() (always re-fetches).


Navigation + Auth

Stack.Protected (Expo Router v5+, recommended)

typescript
// app/_layout.tsx
import { Stack } from 'expo-router'
import { useAuthStore } from '@/stores/authStore'

export default function RootLayout() {
  const session = useAuthStore((s) => s.session)

  return (
    <Stack>
      <Stack.Protected guard={!!session}>
        <Stack.Screen name="(tabs)" />
        <Stack.Screen name="modal" options={{ presentation: 'modal' }} />
      </Stack.Protected>
      <Stack.Protected guard={!session}>
        <Stack.Screen name="sign-in" />
      </Stack.Protected>
    </Stack>
  )
}

When session flips, Expo Router automatically redirects and cleans history.

With XState for complex auth flows

XState manages async auth (token check, refresh, error recovery). Derive a boolean for Stack.Protected:

typescript
const AuthContext = createActorContext(authMachine)

function RootNavigator() {
  const session = AuthContext.useSelector((s) => s.context.session)
  const isChecking = AuthContext.useSelector((s) => s.matches('checking'))

  if (isChecking) return <SplashScreen />

  return (
    <Stack>
      <Stack.Protected guard={!!session}>
        <Stack.Screen name="(app)" />
      </Stack.Protected>
      <Stack.Protected guard={!session}>
        <Stack.Screen name="sign-in" />
      </Stack.Protected>
    </Stack>
  )
}

Lists: FlashList + React Query

FlashList replaces TanStack Virtual for RN — it uses native view recycling instead of DOM-based absolute positioning.

Infinite scroll pattern

typescript
function PostList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isRefetching } =
    useInfiniteQuery({
      queryKey: ['posts'],
      queryFn: ({ pageParam }) => fetchPosts(pageParam),
      initialPageParam: 0,
      getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
    })

  const items = useMemo(() => data?.pages.flatMap((p) => p.items) ?? [], [data])

  const handleEndReached = useCallback(() => {
    if (hasNextPage && !isFetchingNextPage) fetchNextPage()
  }, [hasNextPage, isFetchingNextPage, fetchNextPage])

  return (
    <FlashList
      data={items}
      renderItem={({ item }) => <PostCard post={item} />}
      keyExtractor={(item) => item.id}
      onEndReached={handleEndReached}
      onEndReachedThreshold={0.3}
      ListFooterComponent={isFetchingNextPage ? <ActivityIndicator /> : null}
      refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={refetch} />}
    />
  )
}

The !isFetchingNextPage guard in handleEndReached is essential — FlashList can fire onEndReached multiple times in quick succession, causing duplicate fetches.


TanStack Form in RN

TanStack Form is headless — no DOM dependency, no adapter needed. The key difference is TextInput uses onChangeText (string directly) instead of onChange (event object).

typescript
<form.Field name="username">
  {(field) => (
    <TextInput
      value={field.state.value}
      onChangeText={field.handleChange}   // string directly — no event extraction
      onBlur={field.handleBlur}           // triggers isTouched + onBlur validators
      autoCapitalize="none"
    />
  )}
</form.Field>

For numeric fields, convert at the call site:

typescript
<TextInput
  keyboardType="numeric"
  onChangeText={(val) => field.handleChange(val === '' ? null : Number(val))}
  value={String(field.state.value ?? '')}
/>

Wrap forms in ScrollView with keyboardShouldPersistTaps="handled" — otherwise the first tap on Submit dismisses the keyboard instead of firing the press.


Zustand Persist with MMKV

MMKV is synchronous and runs on the native thread — no async hydration gap.

typescript
import { createMMKV } from "react-native-mmkv";
import { StateStorage, createJSONStorage } from "zustand/middleware";

const mmkv = createMMKV(); // create at module level — never inside a component

const zustandStorage: StateStorage = {
  setItem: (name, value) => mmkv.set(name, value),
  getItem: (name) => mmkv.getString(name) ?? null, // must return null, not undefined
  removeItem: (name) => mmkv.remove(name),
};

export const useAppStore = create<AppState>()(
  persist(
    (set) => ({
      /* state + actions */
    }),
    {
      name: "app-storage",
      storage: createJSONStorage(() => zustandStorage),
      partialize: (state) => ({ token: state.token, theme: state.theme }),
    },
  ),
);

For sensitive data, use an encrypted MMKV instance with encryptionKey. For the hybrid pattern (hardware-backed key + encrypted MMKV), see references/expo-essentials.md.


Animations & Gestures

Reanimated runs animations on the UI thread via worklets. Gesture Handler routes touch events to the same thread. Reanimated 4 adds CSS-style declarative transitions.

Threading Model

Shared values live on the UI thread. Reading .value on the JS thread is a blocking bridge call — never do it in hot paths. Writing is instant.

typescript
const x = useSharedValue(0);

// Gesture callback — runs on UI thread (worklet)
const pan = Gesture.Pan()
  .onChange((e) => {
    "worklet";
    x.value += e.changeX; // instant, no bridge
  })
  .onEnd(() => {
    "worklet";
    x.value = withSpring(0);
    runOnJS(onDragEnd)(); // call JS functions via runOnJS
  });

// Animated style — also runs on UI thread
const style = useAnimatedStyle(() => ({
  transform: [{ translateX: x.value }],
}));

Critical Rules

  • Replace entire values: sv.value = { x: 50 } not sv.value.x = 50 (breaks reactivity)
  • No destructuring: const { x } = sv.value creates a plain number, not reactive
  • No reads during render: shared value reads are side effects, violate Rules of React
  • Shared values don't re-render: if you need React to respond, maintain separate state
  • GestureHandlerRootView must wrap the app root with style={{ flex: 1 }}
  • Android modals need their own GestureHandlerRootView (outside native root view)

Declarative vs Imperative

Use CSS transitions (Reanimated 4) for state-driven style changes (toggle colors, opacity, dimensions). Use layout animations (entering/exiting props) for mount/unmount. Use worklets + shared values for gesture-driven animations and imperative chains.

For detailed patterns, see references/animations.md.


Bottom Sheets

@lodev09/react-native-true-sheet — a native bottom sheet backed by UISheetPresentationController (iOS) and BottomSheetDialog (Android). No Reanimated dependency. New Architecture only.

Imperative Ref-Based Control

typescript
import { TrueSheet } from "@lodev09/react-native-true-sheet";

function MySheet() {
  const sheet = useRef<TrueSheet>(null);

  return (
    <>
      <Button onPress={() => sheet.current?.present()} />
      <TrueSheet ref={sheet} detents={[0.5, 1]}>
        <MyContent />
      </TrueSheet>
    </>
  );
}

Key Rules

  • Imperative control only — use ref.present()/dismiss()/resize(), not state props.
  • Max 3 detents, sorted smallest to largest. Use 'auto' for content-fitting.
  • Never combine scrollable with 'auto' detent — they conflict. Use fixed detents.
  • Never use flex: 1 on sheet content — collapses to zero height. Use flexGrow or fixed height.
  • Always dismiss() before unmounting — the native sheet outlives the React component.
  • Standard TextInput works — no special keyboard component needed (native handling).
  • Standard ScrollView/FlashList works with scrollable + nestedScrollEnabled — auto-detected in v3 (up to 2 levels deep).

For full patterns, platform differences, and Expo Router integration, see references/bottom-sheet.md.


Push Notifications

expo-notifications handles FCM (Android) and APNs (iOS) through Expo's push service.

SDK 53 Breaking Changes

  • Android push does not work in Expo Go — requires development build.
  • Config plugin must be explicit in app.json plugins array.

Key Pattern

typescript
// Must configure or foreground notifications are silently suppressed
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowBanner: true,
    shouldShowList: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
});

Notification Rules

  • Android channels must exist before requesting permissions on Android 13+.
  • Channels are immutable — cannot change importance/vibration after creation.
  • Poll receipts in production — tickets only confirm Expo received the request.
  • Background tasks must be defined at module scope (not inside components).

For permission handling, listeners, and background tasks, see references/notifications.md.


OTA Updates

expo-updates enables over-the-air JavaScript bundle updates without app store releases.

Update Check Pattern

typescript
import * as Updates from "expo-updates";

// Run after first render, not during startup (can freeze UI on slow network)
const check = await Updates.checkForUpdateAsync();
if (check.isAvailable) {
  await Updates.fetchUpdateAsync();
  await Updates.reloadAsync();
}

Emergency Launch Detection

typescript
if (Updates.isEmergencyLaunch) {
  // OTA caused a crash — app rolled back to embedded bundle
  // Log immediately to error tracking
}

Always instrument this — it's your production crash signal for OTA updates.

For staged rollout, runtime versions, and reactive patterns, see references/expo-essentials.md.


File Organization

text
app/
  _layout.tsx              # Root layout — providers, auth guard
  (auth)/
    _layout.tsx            # Auth group layout
    sign-in.tsx
  (app)/
    _layout.tsx            # App group layout (tabs)
    (tabs)/
      _layout.tsx          # Tab navigator
      index.tsx
      profile.tsx
    [id].tsx               # Dynamic route
    modal.tsx              # Modal screen
queries/                   # queryOptions definitions
mutations/                 # useMutation wrappers
machines/                  # XState machine definitions
stores/                    # Zustand stores (MMKV persist)
hooks/                     # useAppState, useOnlineManager, useRefreshOnFocus
components/                # Shared components

Key conventions:

  • Route groups (name)/ organize without URL impact
  • _layout.tsx defines the navigator for each segment
  • Machine definitions are pure TypeScript — no React imports
  • queries/ files export queryOptions objects, not hooks

Common Pitfalls

  1. Missing focusManager/onlineManager setuprefetchOnWindowFocus and offline pausing do nothing without manual AppState and NetInfo wiring.

  2. Awaiting prefetchQuery before router.push — makes navigation feel slow. Fire prefetch without await, let React Query cache serve the destination screen.

  3. Using useGlobalSearchParams instead of useLocalSearchParams — global re-renders on every navigation event. Always prefer local.

  4. useEffect for screen focus refetchuseEffect doesn't re-run when navigating back (screens stay mounted). Use useFocusEffect from expo-router.

  5. getNextPageParam returning null — must return undefined to signal no next page. Coerce API nulls: lastPage.nextCursor ?? undefined.

  6. FlashList v2 requires New Architecture — v2 is a ground-up rewrite for Fabric. estimatedItemSize is deprecated (ignored). overrideItemLayout only supports span.

  7. MMKV getString returns undefined — Zustand's StateStorage.getItem must return null for missing keys. Always coerce: ?? null.

  8. Creating MMKV instance inside a component — creates new instances each render. Declare at module level.

  9. keyboardShouldPersistTaps not set on form ScrollView — first tap dismisses keyboard instead of pressing Submit button.

  10. retry: 3 (default) on mobile — with flaky connections, 3 retries with exponential backoff can take 30+ seconds. Consider retry: 1 for time-sensitive UI.

  11. Mutating shared value propertiessv.value.x = 50 breaks reactivity. Must replace entire value: sv.value = { x: 50, y: 0 }.

  12. GestureHandlerRootView missing flex: 1 — wrapper has zero height. Gestures appear to not work at all.

  13. Unmounting TrueSheet while open — the native sheet does NOT dismiss automatically. Always call dismiss() before removing from tree.

  14. auto detent with scrollable in TrueSheet — auto-sizing and scroll pinning conflict. Use fixed detents when scrollable={true}.

  15. Missing setNotificationHandler — all foreground notifications silently suppressed. No error.

  16. checkForUpdateAsync on slow network — can freeze Android UI for minutes. Run after first render with a manual timeout.

  17. expo-image recyclingKey in FlashList — without it, recycled cells flash the previous item's image.

  18. expo-secure-store biometric invalidation — keys with requireAuthentication become permanently unreadable when biometrics change. No recovery except clearing app data.


Reference Files

File When to read
references/react-query-rn.md Focus/online managers, cache persistence, prefetching
references/expo-router.md Typed routes, layouts, modals, auth, search params
references/lists.md FlashList + React Query, infinite scroll, performance
references/zustand-rn.md MMKV persist adapter, encryption, hydration patterns
references/testing-rn.md RNTL, testing Query/Router/Form/XState, MSW in RN
references/animations.md Reanimated, Gesture Handler patterns and gotchas
references/bottom-sheet.md react-native-true-sheet setup, detents, Expo Router
references/notifications.md expo-notifications permissions, listeners, background
references/expo-essentials.md expo-image, expo-secure-store, expo-haptics, expo-updates

Expand your agent's capabilities with these related and highly-rated skills.

trancong12102/agentskills

deps-dev

Look up the latest stable version of any open-source package across npm, PyPI, Go, Cargo, Maven, and NuGet. Use when the user asks 'what's the latest version of X', 'what version should I use', 'is X deprecated', 'how outdated is my package.json/requirements.txt/Cargo.toml', or needs version numbers for adding or updating dependencies. Also covers pinning versions, checking if packages are maintained, or comparing installed vs latest versions. Do NOT use for private/internal packages or for looking up documentation (use context7).

3 0
Explore
trancong12102/agentskills

github-codebase-search

Semantic search for public GitHub repos without cloning. Use when the user wants to understand how an external library or framework works internally, investigate upstream bugs, trace code paths in a repo they haven't cloned, or search GitHub source code by intent. Do NOT use for local codebase questions (use codebase-search), documentation lookup (use context7), or private repos.

3 0
Explore
trancong12102/agentskills

council-review

Multi-model AI code review — runs Codex, Claude, and Simplify reviews in parallel, then synthesizes a unified report. Use when the user asks to review code changes, audit a diff, check code quality, review a PR, review commits, or review uncommitted changes. Also covers 'code review', 'review my changes', 'check this before I merge', or wanting multiple perspectives on code. Do NOT use for documentation/markdown review or trivial single-line changes.

3 0
Explore
trancong12102/agentskills

react-web-advanced

Web-specific React patterns for type-safe file-based routing, route-level data loading, server-side rendering, search param validation, code splitting, and list virtualization. Use when building React web apps with route loaders, SSR streaming, validated search params, lazy route splitting, or virtualizing large DOM lists. Do not use for React Native apps — use react-native-advanced instead.

3 0
Explore
trancong12102/agentskills

context7

Fetch up-to-date documentation for any open-source library or framework. Use when the user asks to look up docs, check an API, find code examples, or verify how a feature works — especially with a specific library name, version migration, or phrases like 'what's the current way to...' or 'the API might have changed'. Also covers setup and configuration docs. Do NOT use for general programming concepts, internal project code, or version lookups (use deps-dev).

3 0
Explore
trancong12102/agentskills

ast-grep

Guide for writing ast-grep rules to perform structural code search and analysis. Use when users need to search codebases using Abstract Syntax Tree (AST) patterns, find specific code structures, or perform complex code queries that go beyond simple text search. This skill should be used when users ask to search for code patterns, find specific language constructs, or locate code with particular structural characteristics.

3 0
Explore

Didn't find tool you were looking for?

Be as detailed as possible for better results