Skip to main content
expo-mobile-app-rule Specifies best practices and conventions for Expo-based mobile app development.
설치로 이동 Skills Marketplace 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/oimiragieo/agent-studio --skill expo-mobile-app-rule명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... name expo-mobile-app-rule description Specifies best practices and conventions for Expo-based mobile app development. version 1.0.0 model sonnet invoked_by both user_invocable true tools ["Read","Write","Edit"] globs mobile/**/*.tsx best_practices ["Follow the guidelines consistently","Apply rules during code review","Use as reference when writing new code"] error_handling graceful streaming supported verified false lastVerifiedAt "2026-02-19T05:29:09.098Z" source builtin trust_score 100 provenance_sha bd10d0827cce556e
Expo Mobile App Rule Skill
You are a coding standards expert specializing in expo mobile app rule.
You help developers write better code by applying established guidelines and best practices.
- Review code for guideline compliance
- Suggest improvements based on best practices
- Explain why certain patterns are preferred
- Help refactor code to meet standards
When reviewing or writing code, apply these comprehensive mobile app development patterns with Expo.
Navigation with Expo Router
File-Based Routing
Expo Router uses the file system for navigation:
app/
_layout.tsx # Root layout
index.tsx # Home screen (/)
(tabs)/ # Tab navigator group
_layout.tsx # Tab layout
home.tsx # /home
profile.tsx # /profile
user/
[id].tsx # Dynamic route /user/:id
modal.tsx # Can be presented as modal
Root Layout
import { Stack } from 'expo-router' ;
export default function RootLayout ( ) {
return (
<Stack >
<Stack.Screen name ="(tabs)" options ={{ headerShown: false }} />
<Stack.Screen
name ="modal"
options ={{
presentation: 'modal ',
title: 'Settings ',
}}
/>
</Stack >
);
}
Tab Navigation
import { } ;
{ } ;
( ) {
(
);
}
Example usage:
```
User: "Review this code for expo mobile app rule compliance"
Agent: [Analyzes code against guidelines and provides specific feedback]
```
Tabs
from
'expo-router'
import
Ionicons
from
'@expo/vector-icons'
export
default
function
TabLayout
return
<Tabs >
<Tabs.Screen
name ="home"
options ={{
title: 'Home ',
tabBarIcon: ({ color , size }) => (
<Ionicons name ="home" size ={size} color ={color} />
),
}}
/>
<Tabs.Screen
name ="profile"
options ={{
title: 'Profile ',
tabBarIcon: ({ color , size }) => (
<Ionicons name ="person" size ={size} color ={color} />
),
}}
/>
</Tabs >
Navigation Methods import { useRouter, useLocalSearchParams, Link } from 'expo-router' ;
function MyComponent ( ) {
const router = useRouter ();
const params = useLocalSearchParams ();
return (
<>
{/* Declarative navigation */}
<Link href ="/profile" > Go to Profile</Link >
<Link href ={{ pathname: '/user /[id ]', params: { id: '123 ' } }}>
View User
</Link >
{/* Imperative navigation */}
<Button onPress ={() => router.push('/profile')} title="Push" />
<Button onPress ={() => router.replace('/home')} title="Replace" />
<Button onPress ={() => router.back()} title="Go Back" />
</>
);
}
Dynamic Routes
import { useLocalSearchParams } from 'expo-router' ;
export default function UserScreen ( ) {
const { id } = useLocalSearchParams<{ id : string }>();
return <Text > User ID: {id}</Text > ;
}
Protected Routes
import { useAuth } from '@/hooks/useAuth' ;
import { Redirect , Slot } from 'expo-router' ;
export default function AppLayout ( ) {
const { user, loading } = useAuth ();
if (loading) {
return <LoadingScreen /> ;
}
if (!user) {
return <Redirect href ="/login" /> ;
}
return <Slot /> ;
}
State Management
Context API for Global State
import { createContext, useContext, useState, ReactNode } from 'react' ;
interface AppState {
user : User | null ;
theme : 'light' | 'dark' ;
setUser : (user : User | null ) => void ;
setTheme : (theme : 'light' | 'dark' ) => void ;
}
const AppContext = createContext<AppState | undefined >(undefined );
export function AppProvider ({ children }: { children: ReactNode } ) {
const [user, setUser] = useState<User | null >(null );
const [theme, setTheme] = useState<'light' | 'dark' >('light' );
return (
<AppContext.Provider value ={{ user , theme , setUser , setTheme }}>
{children}
</AppContext.Provider >
);
}
export const useApp = ( ) => {
const context = useContext (AppContext );
if (!context) {
throw new Error ('useApp must be used within AppProvider' );
}
return context;
};
Redux Toolkit (for Complex State)
import { createSlice, PayloadAction } from '@reduxjs/toolkit' ;
interface UserState {
currentUser : User | null ;
loading : boolean ;
}
const userSlice = createSlice ({
name : 'user' ,
initialState : { currentUser : null , loading : false } as UserState ,
reducers : {
setUser : (state, action : PayloadAction <User | null > ) => {
state.currentUser = action.payload ;
},
setLoading : (state, action : PayloadAction <boolean > ) => {
state.loading = action.payload ;
},
},
});
export const { setUser, setLoading } = userSlice.actions ;
export default userSlice.reducer ;
Zustand (Lightweight Alternative)
import { create } from 'zustand' ;
import { persist, createJSONStorage } from 'zustand/middleware' ;
import AsyncStorage from '@react-native-async-storage/async-storage' ;
interface AppStore {
user : User | null ;
theme : 'light' | 'dark' ;
setUser : (user : User | null ) => void ;
toggleTheme : () => void ;
}
export const useStore = create<AppStore >()(
persist (
set => ({
user : null ,
theme : 'light' ,
setUser : user => set ({ user }),
toggleTheme : () =>
set (state => ({
theme : state.theme === 'light' ? 'dark' : 'light' ,
})),
}),
{
name : 'app-storage' ,
storage : createJSONStorage (() => AsyncStorage ),
}
)
);
Offline Support
AsyncStorage for Local Data import AsyncStorage from '@react-native-async-storage/async-storage' ;
await AsyncStorage .setItem ('user' , JSON .stringify (user));
const userData = await AsyncStorage .getItem ('user' );
const user = userData ? JSON .parse (userData) : null ;
await AsyncStorage .removeItem ('user' );
await AsyncStorage .clear ();
SQLite for Complex Offline Data import * as SQLite from 'expo-sqlite' ;
class DatabaseService {
private db : SQLite .SQLiteDatabase | null = null ;
async init ( ) {
this .db = await SQLite .openDatabaseAsync ('myapp.db' );
await this .db .execAsync (`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
timestamp INTEGER,
synced INTEGER DEFAULT 0
);
` );
}
async saveMessage (text : string ) {
await this .db ?.runAsync (
'INSERT INTO messages (text, timestamp) VALUES (?, ?)' ,
text,
Date .now ()
);
}
async getUnsynced ( ) {
return await this .db ?.getAllAsync ('SELECT * FROM messages WHERE synced = 0' );
}
async markSynced (id : number ) {
await this .db ?.runAsync ('UPDATE messages SET synced = 1 WHERE id = ?' , id);
}
}
Network State Detection import NetInfo from '@react-native-community/netinfo' ;
import { useEffect, useState } from 'react' ;
function useOnlineStatus ( ) {
const [isOnline, setIsOnline] = useState (true );
useEffect (() => {
const unsubscribe = NetInfo .addEventListener ((state ) => {
setIsOnline (state.isConnected ?? false );
});
return () => unsubscribe ();
}, []);
return isOnline;
}
function MyScreen ( ) {
const isOnline = useOnlineStatus ();
return (
<View >
{!isOnline && <Banner message ="You are offline" /> }
{/* Rest of content */}
</View >
);
}
Offline-First Data Sync import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' ;
function useOfflineData ( ) {
const queryClient = useQueryClient ();
const isOnline = useOnlineStatus ();
const { data } = useQuery ({
queryKey : ['items' ],
queryFn : fetchItems,
enabled : isOnline,
staleTime : Infinity ,
});
const mutation = useMutation ({
mutationFn : createItem,
onMutate : async newItem => {
await queryClient.cancelQueries ({ queryKey : ['items' ] });
const previous = queryClient.getQueryData (['items' ]);
queryClient.setQueryData (['items' ], (old : any ) => [...old, newItem]);
if (!isOnline) {
await saveToQueue (newItem);
}
return { previous };
},
onError : (err, newItem, context ) => {
queryClient.setQueryData (['items' ], context?.previous );
},
});
useEffect (() => {
if (isOnline) {
syncQueue ();
}
}, [isOnline]);
return { data, mutation };
}
Push Notifications
Setup with Expo Notifications import * as Notifications from 'expo-notifications' ;
import * as Device from 'expo-device' ;
import Constants from 'expo-constants' ;
import { Platform } from 'react-native' ;
Notifications .setNotificationHandler ({
handleNotification : async () => ({
shouldShowAlert : true ,
shouldPlaySound : true ,
shouldSetBadge : true ,
}),
});
async function registerForPushNotifications ( ) {
if (!Device .isDevice ) {
alert ('Push notifications only work on physical devices' );
return ;
}
const { status : existingStatus } = await Notifications .getPermissionsAsync ();
let finalStatus = existingStatus;
if (existingStatus !== 'granted' ) {
const { status } = await Notifications .requestPermissionsAsync ();
finalStatus = status;
}
if (finalStatus !== 'granted' ) {
return ;
}
const projectId = Constants .expoConfig ?.extra ?.eas ?.projectId ;
const token = await Notifications .getExpoPushTokenAsync ({ projectId });
await sendTokenToBackend (token.data );
if (Platform .OS === 'android' ) {
Notifications .setNotificationChannelAsync ('default' , {
name : 'default' ,
importance : Notifications .AndroidImportance .MAX ,
vibrationPattern : [0 , 250 , 250 , 250 ],
lightColor : '#FF231F7C' ,
});
}
return token.data ;
}
Handling Notifications import { useEffect, useRef } from 'react' ;
function useNotifications ( ) {
const notificationListener = useRef<Notifications .Subscription >();
const responseListener = useRef<Notifications .Subscription >();
useEffect (() => {
notificationListener.current = Notifications .addNotificationReceivedListener (notification => {
console .log ('Notification received:' , notification);
});
responseListener.current = Notifications .addNotificationResponseReceivedListener (response => {
const data = response.notification .request .content .data ;
if (data.screen ) {
router.push (data.screen as any );
}
});
return () => {
if (notificationListener.current ) {
Notifications .removeNotificationSubscription (notificationListener.current );
}
if (responseListener.current ) {
Notifications .removeNotificationSubscription (responseListener.current );
}
};
}, []);
}
Local Notifications async function scheduleNotification ( ) {
await Notifications .scheduleNotificationAsync ({
content : {
title : 'Reminder' ,
body : 'Time to check your tasks!' ,
data : { screen : '/tasks' },
},
trigger : {
seconds : 60 ,
},
});
}
const identifier = await scheduleNotification ();
await Notifications .cancelScheduledNotificationAsync (identifier);
await Notifications .cancelAllScheduledNotificationsAsync ();
Deep Linking
Configure Deep Links
{
"expo" : {
"scheme" : "myapp" ,
"ios" : {
"associatedDomains" : [ "applinks:myapp.com" ]
} ,
"android" : {
"intentFilters" : [
{
"action" : "VIEW" ,
"autoVerify" : true ,
"data" : [
{
"scheme" : "https" ,
"host" : "myapp.com"
}
] ,
"category" : [ "BROWSABLE" , "DEFAULT" ]
}
]
}
}
}
Handle Deep Links in Expo Router
import * as Linking from 'expo-linking' ;
function useDeepLinking ( ) {
useEffect (() => {
Linking .getInitialURL ().then (url => {
if (url) {
handleDeepLink (url);
}
});
const subscription = Linking .addEventListener ('url' , ({ url } ) => {
handleDeepLink (url);
});
return () => subscription.remove ();
}, []);
}
function handleDeepLink (url : string ) {
const { path, queryParams } = Linking .parse (url);
if (path === 'user' ) {
router.push (`/user/${queryParams?.id} ` );
}
}
Universal Links (iOS) & App Links (Android)
{
"applinks" : {
"apps" : [ ] ,
"details" : [
{
"appID" : "TEAMID.com.company.myapp" ,
"paths" : [ "/user/*" , "/post/*" ]
}
]
}
}
[
{
"relation" : [ "delegate_permission/common.handle_all_urls" ] ,
"target" : {
"namespace" : "android_app" ,
"package_name" : "com.company.myapp" ,
"sha256_cert_fingerprints" : [ "FINGERPRINT" ]
}
}
]
Deep Link from Push Notifications
{
"to" : "ExponentPushToken[xxx]" ,
"title" : "New Message" ,
"body" : "You have a new message" ,
"data" : {
"url" : "myapp://chat/123"
}
}
responseListener.current =
Notifications .addNotificationResponseReceivedListener ((response ) => {
const url = response.notification .request .content .data .url ;
if (url) {
Linking .openURL (url);
}
});
Performance Optimization
Memoization import { memo, useMemo, useCallback } from 'react' ;
const ListItem = memo (({ item, onPress }: Props ) => (
<TouchableOpacity onPress ={() => onPress(item.id)}>
<Text > {item.title}</Text >
</TouchableOpacity >
));
function MyList ({ items }: Props ) {
const sortedItems = useMemo (
() => items.sort ((a, b ) => a.title .localeCompare (b.title )),
[items]
);
const handlePress = useCallback ((id : string ) => {
router.push (`/item/${id} ` );
}, []);
return (
<FlatList
data ={sortedItems}
renderItem ={({ item }) => (
<ListItem item ={item} onPress ={handlePress} />
)}
keyExtractor={(item) => item.id}
/>
);
}
Optimized Lists import { FlashList } from '@shopify/flash-list' ;
<FlashList
data ={items}
renderItem ={({ item }) => <ItemCard item ={item} /> }
estimatedItemSize={100}
// Much faster than FlatList for large lists
/>
Image Optimization import { Image } from 'expo-image' ;
<Image
source ={{ uri: 'https: //example.com /large-image.jpg ' }}
placeholder ={require( '. /placeholder.png ')}
contentFit ="cover"
transition ={200}
cachePolicy ="memory-disk"
style ={{ width: 300 , height: 200 }}
/>
Memory Protocol (MANDATORY) cat .claude/context/memory/learnings.md
After completing: Record any new patterns or exceptions discovered.
ASSUME INTERRUPTION: Your context may reset. If it's not in memory, it didn't happen.