When to use
Use this skill when working with Expo or React Native code. It teaches current best practices and prevents common mistakes that AI agents make with outdated patterns, manual native setup, and deprecated APIs.
Critical Rules
1. Use Expo Router for navigation
Wrong (agents do this):
import { createStackNavigator } from "@react-navigation/stack";
const Stack = createStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
Correct:
app/
_layout.tsx
index.tsx
settings.tsx
Use the app/ directory convention. Files become routes automatically. New Expo projects include
Expo Router by default.
Why: Expo Router is built on React Navigation, provides file-based routing, typed routes, deep linking, and integrates with Expo CLI. Manual React Navigation setup is redundant and misses Expo Router features.
2. Use expo-router layouts for navigation structure
Wrong:
// Each screen manually defines its own layout
Correct:
// app/_layout.tsx
import { Stack } from "expo-router";
export default function RootLayout() {
return <Stack />;
}
Why: _layout.tsx defines shared navigation structure, tabs, stacks, and nested layouts. It
centralizes navigation configuration.
3. Use EAS Build for native builds
Wrong:
expo build:ios
expo build:android
Correct:
eas build --platform ios
eas build --platform android
Why: expo build is deprecated. EAS Build is the current cloud build service with better
caching, credentials management, and native module support.
4. Use Expo config plugins for native configuration
Wrong:
# Manually editing
ios/MyApp/Info.plist
android/app/src/main/AndroidManifest.xml
Correct:
// app.config.ts
import { ConfigPlugin } from "expo/config-plugins";
const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => {
config = withInfoPlist(config, (c) => {
c.modResults["MY_API_KEY"] = apiKey;
return c;
});
return config;
};
export default {
plugins: [["withMyApiKey", { apiKey: process.env.API_KEY }]],
};
Why: Direct edits to native files are overwritten on prebuild. Config plugins modify native files at build time and survive prebuild.
5. Platform-specific code: use platform extensions or Platform.select
Wrong:
if (Platform.OS === "ios") {
return <IOSComponent />;
}
return <AndroidComponent />;
Correct:
MyComponent.ios.tsx
MyComponent.android.tsx
Or:
import { Platform } from "react-native";
const styles = StyleSheet.create({
container: Platform.select({
ios: { paddingTop: 20 },
android: { paddingTop: 0 },
}),
});
Why: Platform file extensions let the bundler include only the correct code per platform. Platform.select keeps conditional logic in one file when appropriate.
6. Use expo-image instead of React Native Image
Wrong:
import { Image } from "react-native";
<Image source={{ uri: url }} style={styles.image} />;
Correct:
import { Image } from "expo-image";
<Image source={url} contentFit="cover" style={styles.image} />;
Why: expo-image adds caching, placeholders, blurhash, and better performance. React Native's Image lacks these features.
7. Use expo-font for custom fonts
Wrong:
# Manual font linking via native projects
Correct:
import * as Font from "expo-font";
await Font.loadAsync({
CustomFont: require("./assets/fonts/CustomFont.ttf"),
});
Why: expo-font handles loading and avoids manual native configuration.
8. Use expo-secure-store for sensitive data
Wrong:
import AsyncStorage from "@react-native-async-storage/async-storage";
await AsyncStorage.setItem("authToken", token);
Correct:
import * as SecureStore from "expo-secure-store";
await SecureStore.setItemAsync("authToken", token);
Why: AsyncStorage is not encrypted. expo-secure-store uses the platform keychain/Keystore for tokens and secrets.
9. Use expo-notifications for push notifications
Wrong:
// Manual Firebase/APNs setup, native configuration
Correct:
import * as Notifications from "expo-notifications";
const token = await Notifications.getExpoPushTokenAsync();
Why: expo-notifications abstracts push setup across iOS and Android. Manual Firebase/APNs configuration is error-prone and platform-specific.
10. Use app.json or app.config.ts for configuration
Wrong:
# Editing Info.plist or AndroidManifest.xml directly
Correct:
{
"expo": {
"name": "MyApp",
"slug": "my-app",
"plugins": ["expo-router"]
}
}
Why: Expo manages native config from app.json/app.config.ts. Direct edits are overwritten on prebuild.
11. Use expo-updates for OTA updates
Wrong:
import codePush from "react-native-code-push";
export default codePush(MyApp);
Correct:
import * as Updates from "expo-updates";
await Updates.checkForUpdateAsync();
Why: expo-updates integrates with EAS and Expo's update service. CodePush requires separate setup and is redundant in Expo projects.
12. Use TypeScript with typed navigation params
Wrong:
router.push("/profile");
Correct:
import { router } from "expo-router";
router.push({ pathname: "/profile/[id]", params: { id: userId } });
Define route params in your route files. Expo Router provides typed route helpers when used with TypeScript.
Why: Typed params prevent runtime errors and improve editor support.
Patterns
- Create routes under
app/with file names that map to URLs - Use
_layout.tsxfor shared layout, tabs, and stacks - Put platform-specific components in
Component.ios.tsxandComponent.android.tsx - Use
expo config pluginsinapp.json/app.config.tsfor any native config change - Use
eas.jsonfor EAS Build profiles
Anti-Patterns
- Do not set up React Navigation manually in Expo projects
- Do not use
expo build(use EAS Build) - Do not edit
ios/orandroid/directly in managed workflow - Do not use AsyncStorage for auth tokens or secrets
- Do not use React Native's Image when expo-image is available
- Do not use CodePush in Expo projects (use expo-updates)