
expo-app-setup
by amandeepmittal
Custom codex skills to trigger from `codex` CLI
SKILL.md
name: expo-app-setup
description: Guidance for building, refactoring, and debugging Expo + React Native apps (including Expo Router). Use when wiring screens/layouts, navigation (tab/stack) scaffolding with _layout.tsx per group, avoiding unwanted redirects in app/index.tsx, theming, data fetching with React Query/fetch, Expo module usage, offline handling, and running local Expo tooling (install/start/lint).
license: MIT
compatibility: Expo CNG project with Expo Router + TypeScript strict; needs Expo CLI/bun/npm for installs and linting; no extra system packages beyond tooling defaults.
metadata:
author: amannhimself.dev
Expo App Setup
Overview
Actionable playbook for adding features, managing boilerplate code, implementing modern navigation patterns using Expo Router, state management, fixing bugs, and shipping UI and logic in cross-platform mobile applications using Expo and React Native projects. Repo defaults: src/ app root with @ alias (see tsconfig.json), Expo Router in src/app, and React Query provider in src/app/_layout.tsx.
Navigation guardrails (avoid common mistakes)
- Every stack group needs its own
_layout.tsx: rootapp/_layout.tsx,app/(tabs)/_layout.tsx, and a_layout.tsxinside each tab folder. - Root
app/index.tsxshould render a landing screen; only addRedirectwhen explicitly requested. - When adding a tab: create
app/(tabs)/<tab>, add_layout.tsxwith aStack(headers off unless needed), addindex.tsx, then register the tab inapp/(tabs)/_layout.tsx. - Keep imports ordered (React/React Native, third-party, then
@/aliases) and follow repo style (2-space indent, double quotes, semicolons).
Canonical scaffold:
src/app/
_layout.tsx // root Stack -> (tabs)
index.tsx // landing screen (no redirect unless requested)
(tabs)/
_layout.tsx // Tabs navigator
home/
_layout.tsx // Stack for Home tab
index.tsx
profile/
_layout.tsx // Stack for Profile tab
index.tsx
Minimal layout snippets:
// src/app/_layout.tsx
// Root layout with React Query provider
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Stack } from "expo-router";
const queryClient = new QueryClient();
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
</Stack>
</QueryClientProvider>
);
}
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="home"
options={{ title: "Home", headerShown: false }}
/>
<Tabs.Screen
name="profile"
options={{ title: "Profile", headerShown: false }}
/>
</Tabs>
);
}
// app/(tabs)/home/_layout.tsx
import { Stack } from "expo-router";
export default function HomeStack() {
return <Stack screenOptions={{ headerShown: false }} />;
}
// app/index.tsx
import { View, Text, StyleSheet } from "react-native";
export default function IndexScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>MovieKnight</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: "center", justifyContent: "center" },
title: { fontSize: 24, fontWeight: "600" },
});
Core workflow
- Clarify the task: screen/flow/bug, target platforms (iOS/Android/web), offline/low-connectivity expectations.
- Locate context: relevant route file, component, provider, and shared utilities/theme.
- Implement using the patterns below; reuse shared components and helpers before adding new ones.
- Verify: lint, run the flow on target platforms, and check loading/error/offline paths.
- Identify routing system (Expo Router vs plain navigation). Mirror existing patterns and folder structure.
Quick start
- For package manager of choice, use
bunorbunx. - Install dependencies with project's tool (
bunx expo installor equivalent). - To start the development server locally, use
bunx expo startorbunx expo start -c. - Lint before shipping (
bunx expo lintor the project’s lint script).
Patterns
- Images/media: Build URLs via helpers (e.g.,
makeImageUrl); handle null paths; useexpo-image. - Platform concerns: Use
Platform.selectfor variant styling/behavior. Avoid Node-only modules; keep code Expo-compatible. - Animations: Keep animations within Reanimated limits; wrap UI in performant components when needed.
- State and props: Type all props; use
PropsWithChildreninstead ofReactNodewhere linted. Keep derived state memoized; avoid heavy work in render. - Accessibility: Add labels on non-obvious pressables, ensure comfortable hit areas, and keep touch targets platform-appropriate.
Components to reuse (ui patterns)
- Posters & fallbacks:
MoviePosterItemusesexpo-image+makeImageUrlwith a gray placeholder; keep sizes 140x210, radius 12,contentFit="cover"andtransition. - Horizontal rows:
MovieRowrenders a titled row withScrollViewandLinkto/(tabs)/(home)/[id]; uses gesture-handlerPressablefor “See all” and blue accent#007AFF. - Gallery:
PosterGalleryfor detail screens; horizontal posters withChevronRightIconandmakeImageUrlguard. - Screen states:
ScreenStatefor loading/error withActivityIndicator#007AFFand centered copy; prefer this instead of ad-hoc spinners. - Hero blocks:
DetailsHero(blurred backdrop viaexpo-image, overlay,MoviePosterHero+GenreChips+MovieFactRow), rounded cards and layered backgrounds. - Conventions: 2-space indent, double quotes,
@/imports,expo-imagefor media,FlatList/ScrollViewwith pull-to-refresh, and link-driven nav (expo-routerLink) for items.
Instructions
1. Project Setup and Navigation
Basic setup
// src/app/_layout.tsx
// Create root layout for the app with React Query provider
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Stack } from "expo-router";
const queryClient = new QueryClient();
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
</QueryClientProvider>
);
}
Create a tab navigator
// src/app/(tabs)/_layout.tsx
// Tab navigator using NativeTabs (expo-router/unstable-native-tabs)
import Ionicons from "@expo/vector-icons/Ionicons";
import {
Icon,
Label,
NativeTabs,
VectorIcon,
} from "expo-router/unstable-native-tabs";
import { Platform } from "react-native";
export default function TabLayout() {
return (
<NativeTabs
backgroundColor={Platform.select({ android: "#FFFFFF" })}
minimizeBehavior="onScrollDown"
iconColor={Platform.select({
android: { default: "#0f172a", selected: "#2563eb" },
})}
labelVisibilityMode="labeled"
labelStyle={Platform.select({
android: {
default: { color: "#0f172a" },
selected: { color: "#2563eb" },
},
})}
indicatorColor={Platform.select({ android: "#e0e7ff" })}
>
<NativeTabs.Trigger name="(home)">
{Platform.select({
ios: <Icon sf={{ default: "film", selected: "film.fill" }} />,
android: (
<Icon src={<VectorIcon family={Ionicons} name="film-outline" />} />
),
})}
<Label>Home</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="settings">
{Platform.select({
ios: (
<Icon
sf={{ default: "gear.circle", selected: "gear.circle.fill" }}
/>
),
android: (
<Icon
src={<VectorIcon family={Ionicons} name="settings-outline" />}
/>
),
})}
<Label>Settings</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="search" role="search">
{Platform.select({
ios: (
<Icon
sf={{ default: "magnifyingglass", selected: "magnifyingglass" }}
/>
),
android: (
<Icon src={<VectorIcon family={Ionicons} name="search" />} />
),
})}
<Label>Search</Label>
</NativeTabs.Trigger>
</NativeTabs>
);
}
Boilerplate screens
// src/app/(tabs)/(home)/index.tsx
// Create a home screen
import { Link } from "expo-router";
import { View } from "react-native";
export default function Home() {
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Link href="/[id]/1">Go to details</Link>
</View>
);
}
// src/app/(tabs)/(home)/[id].tsx
// Create a details screen
import { useLocalSearchParams } from "expo-router";
import { Text, View } from "react-native";
export default function Details() {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>Details for {id}</Text>
</View>
);
}
// src/app/(tabs)/(home)/_layout.tsx
// Create a layout for the home screen
import { Stack } from "expo-router";
import { Platform } from "react-native";
function getIOSVersion(): number {
if (Platform.OS !== "ios") return 0;
return parseInt(Platform.Version as string, 10);
}
function isIOS26OrLater(): boolean {
return getIOSVersion() >= 26;
}
export default function HomeLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{
title: "Home",
headerLargeTitle: true,
headerTransparent: Platform.OS === "ios",
headerBlurEffect: isIOS26OrLater() ? undefined : "regular",
}}
/>
<Stack.Screen
name="[id]"
options={{
title: "Movie Details",
headerTransparent: Platform.OS === "ios",
headerBlurEffect: isIOS26OrLater() ? undefined : "regular",
}}
/>
</Stack>
);
}
// src/app/(tabs)/settings/index.tsx
// Create a settings screen
import { Text, View } from "react-native";
export default function Settings() {
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Text>Edit app/(tabs)/settings.tsx to edit this screen.</Text>
</View>
);
}
// src/app/(tabs)/settings/_layout.tsx
// Create a layout for the settings screen
import { Stack } from "expo-router";
export default function SettingsLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{
title: "Settings",
headerLargeTitle: true,
headerTransparent: true,
}}
/>
</Stack>
);
}
// src/app/(tabs)/search/index.tsx
// Create a search screen
import { Text, View } from "react-native";
export default function Search() {
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Text>Edit app/(tabs)/search/index.tsx to edit this screen.</Text>
</View>
);
}
// src/app/(tabs)/search/_layout.tsx
// Create a layout for the search screen
import { Stack } from "expo-router";
export default function SearchLayout() {
return (
<Stack>
<Stack.Screen
name="index"
options={{
title: "Search",
headerSearchBarOptions: {
placeholder: "Search ...",
placement: "automatic",
onChangeText: () => {},
},
}}
/>
</Stack>
);
}
2. API integration (TMDB example in src/services)
Add config and API calls in src/services with env-driven API key and @/ imports.
// src/services/config.ts
export const TMDB_API_BASE_URL = "https://api.themoviedb.org/3";
export const TMDB_IMAGE_BASE_URL = "https://image.tmdb.org/t/p";
export const TMDB_POSTER_SIZE = "w500";
export const TMDB_API_KEY = process.env.EXPO_PUBLIC_TMDB_API_KEY ?? "";
export const makeImageUrl = (
path?: string | null,
size = TMDB_POSTER_SIZE
): string | null => {
if (!path) return null;
return `${TMDB_IMAGE_BASE_URL}/${size}${path}`;
};
// src/services/movies.ts
import { TMDB_API_BASE_URL, TMDB_API_KEY } from "@/services/config";
export type Movie = {
id: number;
title: string;
overview: string;
poster_path: string | null;
backdrop_path: string | null;
release_date: string;
vote_average: number;
};
type PopularMoviesResponse = { results: Movie[] };
const ensureApiKey = () => {
if (!TMDB_API_KEY) {
throw new Error(
"TMDB API key missing. Set EXPO_PUBLIC_TMDB_API_KEY to fetch movies."
);
}
};
export const fetchPopularMovies = async (page = 1): Promise<Movie[]> => {
ensureApiKey();
const response = await fetch(
`${TMDB_API_BASE_URL}/movie/popular?language=en-US&page=${page}&api_key=${TMDB_API_KEY}`
);
if (!response.ok) {
throw new Error(`TMDB popular movies request failed: ${response.status}`);
}
const data = (await response.json()) as PopularMoviesResponse;
return data.results;
};
export const popularMoviesQuery = (page = 1) => ({
queryKey: ["popularMovies", page],
queryFn: () => fetchPopularMovies(page),
});
3. Project structure
Use a src/ directory for everything, including the Expo Router app/ folder (routes live at src/app). Update tsconfig.json paths to @/* -> ./src/* and set the Expo Router app root (e.g., EXPO_ROUTER_APP_ROOT=src/app in env or expo-router plugin config) so routing resolves correctly.
├── assets/
└── src/
├── app/
│ ├── (tabs)/
│ └── _layout.tsx
├── components/
├── services/
├── providers/
├── constants/
├── utils/
├── hooks/
└── types/
├── app.json/app.config.ts
├── eas.json
└── package.json
Best practices
DO
- Use functional components with React Hooks
- Implement proper error handling and loading states
- Use React Query for data fetching and state management
- Leverage Expo Router for routing
- Optimize list rendering with
FlatList - Handle platform-specific code elegantly
- Use TypeScript for type safety; keep shared domain types in a common
types/module and keep component-prop types next to the component - Test on both iOS and Android platforms
- Implement proper memory management
DON'T
- Use inline styles excessively (use StyleSheet)
- Make API calls without error handling
- Store sensitive data in plain text
- Ignore platform differences
- Create large monolithic components
- Use index as key in lists
- Make synchronous operations
- Ignore battery optimization
- Deploy without testing on real devices
- Forget to unsubscribe from listeners
Verification
- Pre-flight: confirm
_layout.tsxexists at root, tab container, and each tab folder; ensureapp/index.tsxmatches requested behavior (screen by default, redirect only when asked); align imports; run lint. - Lint before shipping (
bun run lintor the project’s lint script). Fix type errors. - Run the affected flow on target platforms. Test offline/poor network if you touched requests.
- Check for regressions in navigation (back/replace), loading/error states, and skeletons/placeholders.
Common commands (adapt to project scripts)
- Install:
bunx expo install/npx expo install(match repo) - Start:
bunx expo start(ornpx expo start --ios/--android/--web) - Lint:
bunx expo lint(ornpx expo lint)
References
- Router scaffolding checklist:
references/router-scaffolding.md.
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon
