xiaohongshu
xiaohongshu
expo-app-setup
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...
Full skill instructions
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.
_layout.tsx: root app/_layout.tsx, app/(tabs)/_layout.tsx, and a _layout.tsx inside each tab folder.app/index.tsx should render a landing screen; only add Redirect when explicitly requested.app/(tabs)/<tab>, add _layout.tsx with a Stack (headers off unless needed), add index.tsx, then register the tab in app/(tabs)/_layout.tsx.@/ 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" },
});
bun or bunx.bunx expo install or equivalent).bunx expo start or bunx expo start -c.bunx expo lint or the project’s lint script).makeImageUrl); handle null paths; use expo-image.Platform.select for variant styling/behavior. Avoid Node-only modules; keep code Expo-compatible.PropsWithChildren instead of ReactNode where linted. Keep derived state memoized; avoid heavy work in render.MoviePosterItem uses expo-image + makeImageUrl with a gray placeholder; keep sizes 140x210, radius 12, contentFit="cover" and transition.MovieRow renders a titled row with ScrollView and Link to /(tabs)/(home)/[id]; uses gesture-handler Pressable for “See all” and blue accent #007AFF.PosterGallery for detail screens; horizontal posters with ChevronRightIcon and makeImageUrl guard.ScreenState for loading/error with ActivityIndicator #007AFF and centered copy; prefer this instead of ad-hoc spinners.DetailsHero (blurred backdrop via expo-image, overlay, MoviePosterHero + GenreChips + MovieFactRow), rounded cards and layered backgrounds.@/ imports, expo-image for media, FlatList/ScrollView with pull-to-refresh, and link-driven nav (expo-router Link) for items.// 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>
);
}
// 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>
);
}
// 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>
);
}
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),
});
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
FlatListtypes/ module and keep component-prop types next to the component_layout.tsx exists at root, tab container, and each tab folder; ensure app/index.tsx matches requested behavior (screen by default, redirect only when asked); align imports; run lint.bun run lint or the project’s lint script). Fix type errors.bunx expo install / npx expo install (match repo)bunx expo start (or npx expo start --ios/--android/--web)bunx expo lint (or npx expo lint)references/router-scaffolding.md.xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience