YMSNOW27032026 expo

Guidelines for building mobile applications using Expo, covering UI design, animations, React context patterns, and native device feature permission (Camera, Location, FileUploads)

install
source · Clone the upstream repo
git clone https://github.com/akhilksap2026/YMSNOW27032026
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/akhilksap2026/YMSNOW27032026 "$T" && mkdir -p ~/.claude/skills && cp -r "$T/replit/.local/skills/expo" ~/.claude/skills/akhilksap2026-ymsnow27032026-expo && rm -rf "$T"
manifest: replit/.local/skills/expo/SKILL.md
source content

Always follow these guidelines when building a mobile application using Expo:

Architecture

  • iOS 26 exists. Use NativeTabs from expo-router for native tab bars with liquid glass support. Use
    isLiquidGlassAvailable()
    from expo-glass-effect to check availability and fall back to classic Tabs with BlurView for older iOS/Android.
  • Follow modern React Native patterns and best-practices.
  • Put as much of the app in the frontend as possible. The backend should only be responsible for data persistence and making API calls.
  • Use React context for state that is shared across multiple components.
  • Use React Query (@tanstack/react-query) for server state fetching.
  • Use useState for very local state.
  • Minimize the number of files. Collapse similar components into a single file.
  • If the app is complex and requires functionality that can't be done in a single request, it is okay to stub out the backend and implement the frontend first.
  • ALWAYS use native device capabilities (camera, location, contacts, etc.) when the app requires them. NEVER use fake/mock data when real device features are available and appropriate.
  • Client-Server Communication: The client (Expo app) interacts with the server (Express app) through a RESTful API. The server is responsible for data storage.
  • Server-side Logic: The server handles API requests, database interactions, authentication, and any other server-specific logic. It's built with Express and uses TypeScript.

Routing

This stack uses Expo Router for file-based routing (similar to Next.js Pages Router) for the frontend. The backend uses Express with TypeScript.

  • Every file in the
    app/
    directory becomes a route
  • Use
    _layout.tsx
    files to define shared layouts
  • Use
    (group)
    directories for layout groups without affecting URLs
  • Use
    [param].tsx
    for dynamic routes

Example structure:

app/
  _layout.tsx          # Root layout with providers
  index.tsx            # Home route (/)
  (tabs)/
    _layout.tsx        # Tab layout
    index.tsx          # First tab (/)
    settings.tsx       # Settings tab (/settings)
  profile/
    _layout.tsx        # Profile stack layout
    index.tsx          # /profile
    [id].tsx           # /profile/:id

For dynamic parameters:

const { id } = useLocalSearchParams()
from "expo-router"

State Management

  • Use React Query for server state (always use object API)
  • Use useState for very local state
  • Avoid props drilling - use React context for shared state
  • Don't wrap <RootLayoutNav/> in a context hook - wrap at the root layout level
  • React Query provider should be the top level provider
  • Use AsyncStorage inside context providers for persistent state

TypeScript Guidance

  • TypeScript first: Proper typing with interfaces and type safety
  • Explicit Type Annotations for useState: Always use
    useState<Type[]>([])
    not
    useState([])
  • Type Verification: Before using any property or method, verify it exists in the type definition
  • Null/Undefined Handling: Use optional chaining (
    ?.
    ) and nullish coalescing (
    ??
    )
  • Complete Object Creation: Include ALL required properties when creating objects
  • Style Properties: Use literal values for variables used in styles (e.g.,
    const fontWeight = "bold" as const
    )

Imports

You can import using

@/
to avoid relative paths (e.g.,
import { Button } from '@/components/Button'
)

Styling

Use react-native's

StyleSheet
for styling.

Networking

  • Use the generated React Query hooks from
    @workspace/api-client-react
    for server data.
    • For any API change, update
      lib/api-spec/openapi.yaml
      first, then run
      pnpm --filter @workspace/api-spec run codegen
      , then use the generated hooks.
    • Do not write app-local React Query hooks, manual
      useQuery
      wrappers, or custom fetch layers for endpoints that already exist in
      @workspace/api-client-react
      .
    • Use the shared QueryClient/fetch setup that ships with the monorepo rather than creating app-local networking infrastructure.
    • The generated hooks return data typed as
      T
      directly.
    • For mutations or requests that do not yet exist in the generated client, update the OpenAPI spec and regenerate rather than bypassing the shared API client package.
    • Do not hardcode domain URLs or hostnames in frontend code. The deployment domain is injected at build time and varies between development, preview, and production environments. Use
      process.env.EXPO_PUBLIC_DOMAIN
      for domain configuration. Routes like
      /api
      or
      /{service}
      are routed to the appropriate artifact/services as needed by the environment.
    • In the root layout file (
      app/_layout.tsx
      ), call
      setBaseUrl(`https://${process.env.EXPO_PUBLIC_DOMAIN}`)
      from
      @workspace/api-client-react
      at the top level (outside any component). Expo bundles run outside the web proxy and need absolute URLs to reach the API server.
    • When the app requires authenticated API calls, use
      setAuthTokenGetter
      from
      @workspace/api-client-react
      to supply a bearer token getter. The getter is called before every request and should return the token string or
      null
      .

App Icon

Generate a custom app icon for the app. Read the mobile-ui skill's app-icon.md reference for guidelines.

Workflow

  • The Expo dev server is accessed via $REPLIT_EXPO_DEV_DOMAIN, not through the shared proxy at localhost:80.
  • Ports are assigned dynamically. Do not hardcode port numbers.
  • Check the configured workflows list for exact workflow names.
  • Use the
    restart_workflow
    tool to restart workflows — not shell commands.
  • The Expo dev server has Hot Module Reloading. Only restart the mobile workflow when you change dependencies or hit a Metro error, not for normal code changes. Restarting unnecessarily wastes time.
  • Backend workflows only requires restart when backend changes were made since it can be a time consuming operation.
  • After presenting the artifact, call
    suggestDeploy()
    so the user knows their app is ready to publish.

React Native Pitfalls

Avoid these common mistakes:

  • UUID Generation:

    • Do not use the 'uuid' package — it requires crypto.getRandomValues() which crashes on iOS/Android
    • Use instead:
      Date.now().toString() + Math.random().toString(36).substr(2, 9)
    • Or use expo-crypto:
      import * as Crypto from 'expo-crypto'; Crypto.randomUUID()
      — pin to version 15.0.x (expo-crypto v55+ crashes in Expo Go)
  • Import Verification:

    • Check existing template files for correct import paths before using them
    • useBottomTabBarHeight is from '@react-navigation/bottom-tabs', not 'expo-router'
    • When unsure, read the actual files to verify exports exist
  • Scrolling Containers — assess whether scrolling is needed:

    • Not everything needs to be scrollable. Fixed layouts (timers, dashboards, single-screen UIs) should use View, not ScrollView
    • FlatList: Add scrollEnabled={data.length > 0} to prevent empty bounce
    • Avoid contentInsetAdjustmentBehavior="automatic" with transparent/large-title headers — it causes over-scrolling
    • ScrollView is for content that exceeds screen height. If content fits, use View
  • useEffect Anti-Patterns:

    • Do not sync props to state with useEffect (causes infinite loops)
    • BAD:
      useEffect(() => setState(prop), [prop])
      then
      useEffect(() => onChange(state), [state])
    • GOOD: Derive values directly from props:
      const hours = Math.floor(value / 3600)
    • Only call onChange on explicit user actions (onPress, onChangeText), not in useEffect
  • Mobile Typography:

    • Maximum font size for display/hero text (clocks, timers, large numbers): 48-64pt (not 96pt)
    • Body text: 14-16pt, Headers: 20-28pt
    • Always test that text fits on a 375pt wide screen (iPhone SE)
  • Empty State Design:

    • Use simple text-based empty states, not placeholder images
    • An icon + descriptive text is sufficient
  • Expo Router Header Configuration:

    • Do not set headerShown dynamically inside screen components — it causes remounts on every re-render
    • BAD: Setting options dynamically inside screen components
    • GOOD: Configure all header options in _layout.tsx where screens are registered
    • Individual screens should only set dynamic content (headerLeft/headerRight buttons), not headerShown
  • Safe Area / Top Padding:

    • Do not use hardcoded top padding (24px, 40px, etc.) — it will be wrong on different devices
    • Use useSafeAreaInsets() from react-native-safe-area-context
    • Example: Use useSafeAreaInsets() hook and apply insets.top to paddingTop
    • This handles iPhone notch, Dynamic Island, and Android status bars automatically
    • Absolutely positioned headers: When a header is positioned absolutely over scrollable content:
      • Header uses: top: insets.top (positions header below notch/Dynamic Island)
      • Content below needs: paddingTop: insets.top + headerContentHeight
      • BAD: paddingTop: 100 (fixed value fails on devices with different insets)
      • GOOD: paddingTop: insets.top + 70 (where 70 = header's internal height)
      • The header height varies by device because insets vary (47px on older iPhones, 59px+ on Dynamic Island)
  • Streaming API Responses (OpenAI, etc.):

    • For streaming, read the mobile-ui skill's expo-fetch.md reference
    • Use
      import { fetch } from 'expo/fetch'
      which supports getReader() on all platforms
  • Keyboard Handling:

    • For detailed keyboard handling patterns (forms, chat, FlatList with inputs), read the mobile-ui skill's keyboard.md reference
    • Use
      react-native-keyboard-controller
      for all keyboard handling — it provides better control than React Native's built-in KeyboardAvoidingView and works consistently across iOS and Android
    • Do not use InputAccessoryView — it doesn't render properly in Expo Go
    • Forms with multiple inputs: Use
      KeyboardAwareScrollViewCompat
      with
      bottomOffset
      and
      keyboardShouldPersistTaps="handled"
    • Chat/messaging: Use
      KeyboardAvoidingView
      from
      react-native-keyboard-controller
      with
      behavior="padding"
      on both platforms,
      keyboardDismissMode="interactive"
      and
      keyboardShouldPersistTaps="handled"
      on FlatList
    • keyboardVerticalOffset:
      0
      for transparent/no header,
      headerHeight
      for opaque header
    • Use useSafeAreaInsets() for bottom padding on the input container to avoid home indicator overlap
    • Do not nest KeyboardAvoidingViews — only one should wrap your content
    • For chat UIs: Use inverted FlatList (see the chat apps guidance below) — no additional scroll logic needed
      • Do not try to implement auto-scroll with scrollToEnd() — it has timing bugs
      • Inverted FlatList handles this automatically — newest message always visible
  • Chat Apps — state and FlatList patterns:

    • For chat app implementation patterns (stale closures, inverted FlatList, streaming), read the mobile-ui skill's expo-fetch.md reference
    • Key rules: Use inverted FlatList (not scrollToEnd), capture state before async operations, use ListHeaderComponent for typing indicator
    • FlatList Boolean Props — type coercion:
      • FlatList props like scrollEnabled, showsVerticalScrollIndicator expect boolean values
      • Using expressions like
        someString || anotherValue
        can pass a string instead of boolean
      • This causes: TypeError: expected dynamic type 'boolean', but had type 'string'
      • Coerce to boolean with !! when using string variables in boolean contexts
      • BAD:
        const showFooter = isSending || streamingContent;
        (streamingContent is string)
      • GOOD:
        const showFooter = isSending || !!streamingContent;
        (!! converts to boolean)
      • BAD:
        scrollEnabled={data || someString}
      • GOOD:
        scrollEnabled={!!data || !!someString}
  • RevenueCat compatibility:

    • RevenueCat works in Expo Go and does not require a native build. In Expo Go, the SDK automatically runs in Preview API Mode, replacing native calls with JavaScript mocks so your app loads without errors.
    • RevenueCat works on the web out of the box without any additional configuration.
  • Custom ErrorBoundary component:

    • Use reloadAppAsync function from expo in tandem with the ErrorBoundary component to restart the app when the app crashes.
      import { reloadAppAsync } from 'expo'
      . Do not use reloadAsync from "expo-updates" for this purpose.
    • The ErrorBoundary is a minimal class component (required by React's error boundary API) with a functional ErrorFallback component for the UI. The consuming component should remain functional.
    • Do not add local state to the ErrorFallback component because it only renders if the app crashes. It should be used as is, unless the user requests it. (Note: dev-mode-only state guarded by
      __DEV__
      is acceptable for debugging features.)
    • The ErrorFallback component uses useColorScheme and useSafeAreaInsets for proper theming and positioning. The ErrorBoundary wrapper uses React's class component error boundary API (getDerivedStateFromError, componentDidCatch).
  • react-native-maps:

    • Pin version to exactly 1.18.0 in package.json (e.g.,
      "react-native-maps": "1.18.0"
      ) — this is the only version compatible with Expo Go currently. Other versions will cause crashes or compatibility issues.
    • Do not add react-native-maps to the plugins array in app.json — it will crash the app.
  • Over use of text:

    • Mobile apps should be designed for touch input, not text input.
    • Most buttons should not have text. They should have icons.
    • If you have to use text, use it sparingly and only for very important information.
    • Applications do not need a title like "Chatbot" or "AI Assistant". They should look like market leading apps.

Gestures

Use PanResponder from 'react-native' for gesture handling.

Safe Area View

When to use SafeAreaView:

  1. Built-in tabs or header: Don't add SafeAreaView - they handle insets automatically
  2. Custom header: Add SafeAreaView to the header component
  3. Removed header: Add SafeAreaView inside a View with background color (not just white space)
  4. Pages inside stacks: Don't add SafeAreaView if parent _layout.tsx has header enabled

Games and absolute positioning:

  • Account for safe area insets in positioning calculations
  • Use useSafeAreaInsets() hook to get inset values
  • Apply insets to positioning calculations in game physics
  • Avoid using SafeAreaView in game screens - factor insets into game loop instead

Font Loading and SplashScreen

The scaffold ships with

@expo-google-fonts/inter
pre-installed and already loaded in
_layout.tsx
with the correct
useFonts
+
SplashScreen
font-gating pattern. Available weights:
Inter_400Regular
,
Inter_500Medium
,
Inter_600SemiBold
,
Inter_700Bold
.

If the user wants a different font, swap the import and

useFonts
call — keep the same font-gating pattern.

Rules:

  • Do NOT remove the font-gating pattern from
    _layout.tsx
  • Handle
    fontError
    alongside
    fontsLoaded
    — if font loading fails, the app should still render rather than showing a white screen forever
  • Tie
    SplashScreen.hideAsync()
    to
    [fontsLoaded, fontError]
    — do not call it unconditionally in
    useEffect([], [])
  • The
    if (!fontsLoaded && !fontError) return null
    must come after the
    useEffect
    hook, not before it (hooks cannot be called conditionally)

Library Compatibility

  • ONLY use libraries from the Expo Go compatible list
  • Do not install additional native libraries unless they're JS only
  • Pre-installed: expo-router, @expo/vector-icons, @tanstack/react-query, react-native-reanimated
  • Error boundary: Custom ErrorBoundary component (components/ErrorBoundary.tsx)

Insets

Web-only insets for web edge cases (always implement for every app):

  • Apply on web only (Platform.OS === "web")
  • Always add at least a 67px top inset to top-level header content for the status bar. Additional vertical padding will likely still be required.
  • Always add a 34px bottom insets
    • If using a bottom tab bar, instead modify the tab bar height to be 84px (50px + 34px). Do not add paddingBottom to the tab bar.
  • Do not add these insets on iOS/Android; native safe areas already handle them
  • Before finishing any screen, verify that web platform insets handling is implemented and are correct.

Web Compatibility

Write code that does not crash in React Native Web. Account for platform-specific compatibility.

Polyfilled APIs (work on web without Platform checks):

  • expo-secure-store, expo-haptics, react-native-maps, Alert, RefreshControl

Partial Web Support:

  • expo-camera (no switch camera, no recordAsync)
  • expo-clipboard, expo-file-system, expo-image, expo-notifications, expo-video
  • react-native-reanimated: Layout animations don't work on web, native driver not supported

No Web Support (use Platform.OS checks):

  • expo-battery, expo-brightness, expo-contacts, expo-device, expo-local-authentication
  • expo-location (use web geolocation API), expo-media-library, expo-sensors, expo-sharing

Example workaround:

if (Platform.OS !== 'web') {
  // Native-only code
} else {
  // Web alternative
}

Payments

If prompted to add payments (i.e. subscriptions, in-app purchases, etc.), always use RevenueCat. Do not use Stripe unless the user explicitly requests it.

Testing

  • Add testID to interactive elements for testing
  • Use mobile screen sizes for automated testing
  • Recommended dimensions: height: 720, width: 400

Replit Environment

  • User can scan QR code from Replit's URL bar menu to test on their physical device via Expo Go
  • Hot module reloading (HMR) is enabled - no need to restart the dev server for code changes

Forbidden Changes

  • NEVER change bundle identifiers after initial setup unless user explicitly requests it.
  • NEVER downgrade the version of React Native or Expo that is declared in package.json.
  • NEVER run
    npx expo start
    or
    npx expo
    directly in a shell. Use the
    restart_workflow
    tool instead — running expo directly will miss environment variables (like
    PORT
    ) injected into the workflow.

References

Before writing code, identify whether any reference below applies to the task. If it does, read it first.

  • references/react_context.md
    - Use this reference when creating or modifying shared state with React context, provider composition, or context-based hooks.
  • references/design_and_aesthetics.md
    - Use this reference when designing or restyling UI, selecting iconography, or implementing animations and visual polish.
  • references/device_features_and_permissions.md
    - Use this reference when implementing camera, location, notifications, contacts, file uploads, or any permission request/denial flow.
  • mobile-ui skill's
    references/keyboard.md
    - Use this reference when implementing any keyboard handling — forms with multiple inputs, chat/messaging UIs, FlatList with inputs, or keyboard utilities (dismiss, detect visibility).
  • mobile-ui skill's
    references/sheets.md
    - Use this reference when implementing modals, sheets, formSheet presentations, auth flows (login/register), wizards, or overlay UI.
  • mobile-ui skill's
    references/tabs.md
    - Use this reference when implementing tab bars — covers NativeTabs with liquid glass support (SDK 54+) and classic Tabs fallback with detailed code examples.