MCPInstall

expo-mobile-app-rule

Specifies best practices and conventions for Expo-based mobile app development.

Installs
79
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/oimiragieo/agent-studio --skill expo-mobile-app-rule
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads expo-mobile-app-rule automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

Expo Mobile App Rule Skill

<identity> 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. </identity> <capabilities> - Review code for guideline compliance - Suggest improvements based on best practices - Explain why certain patterns are preferred - Help refactor code to meet standards </capabilities> <instructions> 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

// app/_layout.tsx
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

// app/(tabs)/_layout.tsx
import { 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

// app/user/[id].tsx
import { useLocalSearchParams } from 'expo-router';

export default function UserScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();

  return <Text>User ID: {id}</Text>;
}

Protected Routes

// app/_layout.tsx
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

// contexts/AppContext.tsx
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)

// store/slices/userSlice.ts
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)

// store/useStore.ts
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';

// Save data
await AsyncStorage.setItem('user', JSON.stringify(user));

// Load data
const userData = await AsyncStorage.getItem('user');
const user = userData ? JSON.parse(userData) : null;

// Remove data
await AsyncStorage.removeItem('user');

// Clear all
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;
}

// Usage
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,
    // Use cached data when offline
    staleTime: Infinity,
  });

  const mutation = useMutation({
    mutationFn: createItem,
    onMutate: async newItem => {
      // Optimistic update
      await queryClient.cancelQueries({ queryKey: ['items'] });
      const previous = queryClient.getQueryData(['items']);

      queryClient.setQueryData(['items'], (old: any) => [...old, newItem]);

      // Save to local storage if offline
      if (!isOnline) {
        await saveToQueue(newItem);
      }

      return { previous };
    },
    onError: (err, newItem, context) => {
      // Rollback on error
      queryClient.setQueryData(['items'], context?.previous);
    },
  });

  // Sync queue when online
  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';

// Configure notification handler
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 });

  // Send token to your backend
  await sendTokenToBackend(token.data);

  // Android-specific channel setup
  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(() => {
    // Foreground notification handler
    notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
      console.log('Notification received:', notification);
    });

    // User interaction handler
    responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
      const data = response.notification.request.content.data;

      // Navigate based on notification 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,
      // Or use specific date
      // date: new Date(Date.now() + 60 * 60 * 1000),
      // Or repeating
      // repeats: true,
    },
  });
}

// Cancel notification
const identifier = await scheduleNotification();
await Notifications.cancelScheduledNotificationAsync(identifier);

// Cancel all
await Notifications.cancelAllScheduledNotificationsAsync();

Deep Linking

Configure Deep Links

// app.json
{
  "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

// Expo Router handles deep links automatically
// myapp://user/123 -> app/user/[id].tsx

// For custom handling:
import * as Linking from 'expo-linking';

function useDeepLinking() {
  useEffect(() => {
    // Get initial URL (app opened via link)
    Linking.getInitialURL().then(url => {
      if (url) {
        handleDeepLink(url);
      }
    });

    // Listen for URL changes (app already open)
    const subscription = Linking.addEventListener('url', ({ url }) => {
      handleDeepLink(url);
    });

    return () => subscription.remove();
  }, []);
}

function handleDeepLink(url: string) {
  const { path, queryParams } = Linking.parse(url);

  // Navigate based on path
  if (path === 'user') {
    router.push(`/user/${queryParams?.id}`);
  }
}

Universal Links (iOS) & App Links (Android)

// apple-app-site-association (serve at https://myapp.com/.well-known/)
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.company.myapp",
        "paths": ["/user/*", "/post/*"]
      }
    ]
  }
}
// assetlinks.json (serve at https://myapp.com/.well-known/)
[
  {
    "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

// When sending push notification from backend
{
  "to": "ExponentPushToken[xxx]",
  "title": "New Message",
  "body": "You have a new message",
  "data": {
    "url": "myapp://chat/123"
  }
}

// Handle in app
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 }}
/>
</instructions> <examples> Example usage: ``` User: "Review this code for expo mobile app rule compliance" Agent: [Analyzes code against guidelines and provides specific feedback] ``` </examples>

Memory Protocol (MANDATORY)

Before starting:

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.

FAQ

What does the expo-mobile-app-rule skill do?

Specifies best practices and conventions for Expo-based mobile app development.

How do I install expo-mobile-app-rule?

Run npx skills add https://github.com/oimiragieo/agent-studio --skill expo-mobile-app-rule in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/expo-mobile-app-rule/ (personal) or .claude/skills/expo-mobile-app-rule/ (project).

Which AI agents can use expo-mobile-app-rule?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is expo-mobile-app-rule safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains expo-mobile-app-rule?

It is published by oimiragieo in oimiragieo/agent-studio. It has 79 recorded installs.

More from oimiragieo/agent-studio

View all →

pyqt6-ui-development-rules

oimiragieo/agent-studio

PyQt6 desktop GUI development rules -- signal/slot architecture, QSS theming, QThread concurrency, layout management, and cross-platform rendering. Enforces MVC separation and responsive UI patterns.

Frontend & Design1.1K installs

filesystem

oimiragieo/agent-studio

File system operations guidance - read, write, search, and manage files using Claude Code's built-in tools.

729 installs

chrome-browser

oimiragieo/agent-studio

Open the skill to read its SKILL.md instructions.

slack-notifications

oimiragieo/agent-studio

Open the skill to read its SKILL.md instructions.

context-compressor

oimiragieo/agent-studio

Open the skill to read its SKILL.md instructions.

268 installs

nodejs-expert

oimiragieo/agent-studio

Open the skill to read its SKILL.md instructions.

Backend & APIs196 installs

Popular mobile skills

View category →

design-mobile-apps

designed-by-ai/skills

Design mobile app screens with Sleek, edit Sleek projects, and implement their designs in React Native or HTML.

Mobile1.1M installs

ios-design

uizze.sh

Design and review iOS UI/UX for iPhone apps in SwiftUI, UIKit, or React Native. Use for mobile app design, screen redesigns, navigation flows, typography, forms, keyboard and safe-area layout, and accessibility checks. Apply Apple Human Interface Guidelines and consult real product references through the optional Uizze connection.

Mobile416.5K installs

vercel-react-native-skills

vercel-labs/agent-skills

React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.

OfficialMobile256.7K installs

animate-expo

emilkowalski/skills

Build animations in React Native and Expo, making the decisions in the order that determines whether they feel right — should it animate, which thread it runs on, which properties, spring or timing, how the gesture hands off, how it degrades. Writes the implementation with Reanimated, Gesture Handler, Expo Router and expo-haptics. Use when animating anything in an Expo app, adding gestures, sheets, screen transitions, press feedback or haptics, or fixing motion that stutters on device. For web animation use animate.

Mobile140.1K installs

xcode-project-setup

firebase/agent-skills

Safely modifies Xcode projects (.pbxproj) to add Swift Packages and link files. Use this skill whenever an iOS project needs dependencies installed (e.g. Firebase, Alamofire).

OfficialMobile131.2K installs

firebase-remote-config-basics

firebase/agent-skills

Manages Firebase Remote Config templates, feature flags, loading strategies, and SDKs (Android, iOS). Use when downloading/deploying remoteconfig JSON templates, managing version history/feature flags, setting in-app defaults, fetchAndActivate(), real-time listeners, or SDK setup. Don't use for Firebase Hosting, Auth, Firestore, Data Connect, Crashlytics, or App Hosting.

OfficialMobile121.7K installs