Skip to main content
expo-mobile-app-rule Specifies best practices and conventions for Expo-based mobile app development.
Ir a la instalación Skills Marketplace Descubre y explora habilidades de IA creadas por la comunidad.
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Copiar promptMostrar detalles del prompt Un comando directo omite el prompt de revisión. Revisa el origen antes de ejecutarlo.
npx skills add https://github.com/oimiragieo/agent-studio --skill expo-mobile-app-ruleEl comando permanece en una sola línea. Desplázate horizontalmente para revisarlo antes de copiarlo.
¿Prefieres una copia local? Descarga los archivos que SkillsMP tiene disponibles ahora.
Descargar Zip Descargando... Explorador de archivos
10 archivos Ocupaciones relacionadas SOC
Basado en la clasificación ocupacional SOC
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.