expo-patterns
Comprehensive Expo/React Native patterns - NativeTabs, SF Symbols, Apple HIG, EAS builds, animations, and SDK patterns
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
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
appdirectory 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
-
namemust match route name exactly (including parentheses) - Place search tab last to combine with search bar
- Use
roleprop: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
useWindowDimensionsoverDimensions.get()
Text
- Add
selectableprop 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
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-03-04
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!