expo-patterns

Complete guide for building beautiful cross-platform mobile apps with Expo. Combines official Expo skills with foundational patterns.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "expo-patterns" with this command: npx skills add 5dlabs/cto/5dlabs-cto-expo-patterns

Expo Mobile Patterns

Complete guide for building beautiful cross-platform mobile apps with Expo. Combines official Expo skills with foundational patterns.

Running the App

CRITICAL: Always try Expo Go first before creating custom builds.

Most Expo apps work in Expo Go without any custom native code.

npx expo start # Scan QR with Expo Go

When Custom Builds Are Required

You need npx expo run:ios/android or eas build ONLY when using:

  • Local Expo modules (custom native code in modules/ )

  • Apple targets (widgets, app clips via @bacons/apple-targets )

  • Third-party native modules not in Expo Go

  • Custom native configuration

Core Stack

Component Library Purpose

Framework Expo SDK Native modules

Navigation Expo Router File-based routing

CLI Expo CLI / EAS CLI Development and builds

Build EAS Build Cloud builds

Deploy EAS Submit/Update Store submission, OTA

State TanStack Query, Zustand Server and client state

Animation Reanimated 60fps native animations

Gestures Gesture Handler Native touch handling

Icons expo-symbols SF Symbols

Context7 Library IDs

Query these for current best practices:

  • Expo: expo

  • React Native: /facebook/react-native

  • Better Auth: /better-auth/better-auth

Library Preferences

Old/Avoid Use Instead

expo-av

expo-audio and expo-video

expo-permissions

Individual package permission APIs

@expo/vector-icons

expo-symbols (SF Symbols)

AsyncStorage

expo-sqlite/localStorage/install

expo-app-loading

expo-splash-screen

Platform.OS

process.env.EXPO_OS

React.useContext

React.use

Intrinsic img

expo-image Image component

expo-linear-gradient

experimental_backgroundImage

  • CSS gradients

React Native SafeAreaView

react-native-safe-area-context

Code Style

  • Always use kebab-case for file names: comment-card.tsx

  • Routes belong in the app directory only

  • Never co-locate components in the app directory

  • Configure tsconfig.json with path aliases

  • Ensure app always has a route matching "/"

Native Tabs (SDK 54+)

Always prefer NativeTabs from expo-router/unstable-native-tabs :

import { NativeTabs, Icon, Label, Badge } from "expo-router/unstable-native-tabs";

export default function TabLayout() { return ( <NativeTabs minimizeBehavior="onScrollDown"> <NativeTabs.Trigger name="index"> <Label>Home</Label> <Icon sf="house.fill" /> <Badge>9+</Badge> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <Icon sf="gear" /> <Label>Settings</Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="(search)" role="search"> <Label>Search</Label> </NativeTabs.Trigger> </NativeTabs> ); }

NativeTabs Rules

  • Include a trigger for each tab

  • name must match route name exactly (including parentheses)

  • Place search tab last to combine with search bar

  • Use role prop: search , favorites , more , etc.

Icon Component

<Icon sf="house.fill" /> // SF Symbol only <Icon sf="house.fill" drawable="ic_home" /> // With Android drawable <Icon src={require('./icon.png')} /> // Custom image <Icon sf={{ default: "house", selected: "house.fill" }} /> // State variants

SF Symbols (expo-symbols)

Use SF Symbols for native feel. Never use FontAwesome or Ionicons.

import { SymbolView } from "expo-symbols"; import { PlatformColor } from "react-native";

<SymbolView tintColor={PlatformColor("label")} resizeMode="scaleAspectFit" name="square.and.arrow.down" style={{ width: 16, height: 16 }} />

Common Icons

Category Icons

Navigation house.fill , gear , magnifyingglass , plus , xmark , chevron.left/right

Media play.fill , pause.fill , stop.fill , speaker.wave.2.fill

Social heart , heart.fill , star , star.fill , person , person.fill

Actions square.and.arrow.up (share), doc.on.doc (copy), trash , pencil

Status checkmark.circle.fill , xmark.circle.fill , exclamationmark.triangle

Animated Symbols

<SymbolView name="checkmark.circle" animationSpec={{ effect: { type: "bounce", direction: "up" } }} />

Effects: bounce , pulse , variableColor , scale

Expo Router Navigation

Basic Layouts

// app/_layout.tsx - Root layout with Stack import { Stack } from 'expo-router'; export default function RootLayout() { return <Stack />; }

// app/(tabs)/_layout.tsx - Tab navigation import { Tabs } from 'expo-router'; export default function TabLayout() { return <Tabs />; }

Link Navigation

import { Link, router } from 'expo-router';

// Declarative <Link href="/profile">Go to Profile</Link>

// With custom component <Link href="/path" asChild> <Pressable>...</Pressable> </Link>

// Programmatic router.push('/settings'); router.replace('/home'); router.back();

Link Previews & Context Menus

<Link href="/settings"> <Link.Trigger> <Pressable><Card /></Pressable> </Link.Trigger> <Link.Preview /> <Link.Menu> <Link.MenuAction title="Share" icon="square.and.arrow.up" onPress={handleShare} /> <Link.MenuAction title="Delete" icon="trash" destructive onPress={handleDelete} /> </Link.Menu> </Link>

Modal & Sheet

// Modal <Stack.Screen name="modal" options={{ presentation: "modal" }} />

// Sheet with liquid glass (iOS 26+) <Stack.Screen name="sheet" options={{ presentation: "formSheet", sheetGrabberVisible: true, sheetAllowedDetents: [0.5, 1.0], contentStyle: { backgroundColor: "transparent" }, }} />

Dynamic Routes

// app/[id].tsx import { useLocalSearchParams } from 'expo-router'; export default function Detail() { const { id } = useLocalSearchParams(); return <View>...</View>; }

Common Route Structure

app/ _layout.tsx — <NativeTabs /> (index,search)/ _layout.tsx — <Stack /> index.tsx — Main list search.tsx — Search view i/[id].tsx — Detail view

Styling (Apple HIG)

  • Prefer flex gap over margin/padding

  • Inline styles, not StyleSheet.create (unless reusing)

  • Use { borderCurve: 'continuous' } for rounded corners

  • Use CSS boxShadow , NEVER legacy React Native shadow

<View style={{ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }} />

Responsiveness

  • Use <ScrollView contentInsetAdjustmentBehavior="automatic" /> instead of SafeAreaView

  • Apply to FlatList and SectionList too

  • Use flexbox, not Dimensions API

  • ALWAYS prefer useWindowDimensions over Dimensions.get()

Text

  • Add selectable prop for copyable data

  • Use { fontVariant: 'tabular-nums' } for counters

Reanimated Animations

import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';

function AnimatedBox() { const offset = useSharedValue(0);

const animatedStyles = useAnimatedStyle(() => ({ transform: [{ translateX: withSpring(offset.value * 255) }], }));

return <Animated.View style={[styles.box, animatedStyles]} />; }

Gesture Handling

import { GestureDetector, Gesture } from 'react-native-gesture-handler';

function SwipeableCard() { const gesture = Gesture.Pan() .onUpdate((event) => { offset.value = event.translationX; }) .onEnd(() => { offset.value = withSpring(0); });

return ( <GestureDetector gesture={gesture}> <Animated.View style={animatedStyles} /> </GestureDetector> ); }

Visual Effects

// Blur import { BlurView } from 'expo-blur'; <BlurView intensity={50} style={StyleSheet.absoluteFill} />

// Liquid Glass (iOS 26+) import { GlassView } from 'expo-glass-effect'; <GlassView style={styles.glass} cornerRadius={16} />

Common Expo SDK Patterns

// Camera import { CameraView, useCameraPermissions } from 'expo-camera';

// Notifications
import * as Notifications from 'expo-notifications';

// Location import * as Location from 'expo-location';

// Secure Storage import * as SecureStore from 'expo-secure-store';

// Haptics (iOS) import * as Haptics from 'expo-haptics';

// Splash Screen import * as SplashScreen from 'expo-splash-screen'; SplashScreen.preventAutoHideAsync(); SplashScreen.hideAsync();

App Configuration

// app.config.ts export default { expo: { name: "MyApp", slug: "my-app", version: "1.0.0", ios: { bundleIdentifier: "com.company.myapp", supportsTablet: true, }, android: { package: "com.company.myapp", adaptiveIcon: { foregroundImage: "./assets/adaptive-icon.png", backgroundColor: "#ffffff" } }, plugins: [ "expo-router", ["expo-splash-screen", { backgroundColor: "#ffffff" }], ] } };

EAS Build Configuration

// eas.json { "build": { "development": { "developmentClient": true, "distribution": "internal", "ios": { "simulator": true } }, "preview": { "distribution": "internal" }, "production": { "autoIncrement": true } }, "submit": { "production": { "ios": { "appleId": "...", "ascAppId": "..." }, "android": { "track": "internal" } } } }

Build Commands

npm install -g eas-cli && eas login

Development build

eas build --platform ios --profile development eas build --platform android --profile development

Production build

eas build --platform all --profile production

Submit to stores

eas submit --platform ios eas submit --platform android

OTA Update (no app store review!)

eas update --branch production --message "Bug fix"

Environment Variables

.env (EXPO_PUBLIC_ prefix required)

EXPO_PUBLIC_API_URL=https://api.example.com

Access in code

const apiUrl = process.env.EXPO_PUBLIC_API_URL;

EAS environments

eas env:pull --environment development

Validation Commands

npx tsc --noEmit # Type check npx eslint . # Lint npm test # Tests npx expo-doctor # Doctor check npx expo start # Start dev

Mobile Best Practices

  • Expo Go first - only create custom builds when needed

  • Use Expo Router for navigation (file-based, automatic deep linking)

  • Respect platform conventions (iOS HIG, Material Design)

  • 60fps animations with Reanimated worklets

  • Handle safe areas with contentInsetAdjustmentBehavior="automatic"

  • Use expo-haptics conditionally on iOS

  • Offline-first with proper loading/error states

  • Test on real devices via EAS internal distribution

  • Accessibility - use accessibilityLabel , accessibilityRole

  • OTA updates - use EAS Update for instant bug fixes

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

better-auth-expo

No summary provided by upstream source.

Repository SourceNeeds Review
General

elysia-llm-docs

No summary provided by upstream source.

Repository SourceNeeds Review
General

expo-llm-docs

No summary provided by upstream source.

Repository SourceNeeds Review