Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/CodySwannGT/lisa --skill expo-router-best-practices명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SOC 직업 분류 기준
SKILL.md 표시 중
| name | expo-router-best-practices |
| description | creating new routes… |
This skill provides guidance for implementing file-based routing with Expo Router following established best practices and official documentation patterns.
Route files in the app/ directory should be minimal pass-throughs to feature screen components. Business logic and complex UI components belong in feature directories, not route files.
// app/players/[playerId]/compare.tsx - CORRECT
import { Main } from "@/features/compare-players/screens/Main";
/**
* Compare players route.
* URL: /players/[playerId]/compare
*/
export default function CompareScreen() {
return <Main />;
}
// app/players/[playerId]/compare.tsx - INCORRECT
export default function CompareScreen() {
const { playerId } = useLocalSearchParams();
const [data, setData] = useState(null);
// ... 200 lines of business logic
return <ComplexUI />;
}
Use descriptive names for route components, not generic names.
// CORRECT
export default function CompareScreen() { ... }
export default function PlayerDetailScreen() { ... }
export default function SettingsScreen() { ... }
// INCORRECT
export default function Screen() { ... }
export default function Page() { ... }
export default function Index() { ... } // only acceptable for index.tsx files
Include the URL pattern in route file documentation.
/**
* Player detail route.
* URL: /players/[playerId]
*/
export default function PlayerDetailScreen() {
return <Main />;
}
app/
├── _layout.tsx # Root layout (initialization, providers)
├── index.tsx # Default route (/)
├── +not-found.tsx # 404 handling
├── +html.tsx # Web HTML customization (optional)
├── (tabs)/ # Tab navigator group
│ ├── _layout.tsx # Tab configuration
│ ├── index.tsx # Default tab
│ ├── feed/ # Stack within tab
│ │ ├── _layout.tsx
│ │ ├── index.tsx
│ │ └── [postId].tsx
│ └── settings.tsx
├── (auth)/ # Auth screens group
│ ├── sign-in.tsx
│ └── create-account.tsx
└── modal.tsx # Modal route
| Notation | Purpose | Example | URL |
|---|---|---|---|
file.tsx | Static route | about.tsx | /about |
[param].tsx | Dynamic route | [userId].tsx | /123 |
[...slug].tsx | Catch-all route | [...path].tsx | /a/b/c |
(group)/ | Route group (no URL impact) | (tabs)/ | / |
index.tsx | Default route | feed/index.tsx | /feed |
_layout.tsx | Layout definition | (tabs)/_layout.tsx | - |
+not-found.tsx | 404 handler | +not-found.tsx | - |
The root _layout.tsx replaces App.jsx/tsx. Place initialization code here.
// app/_layout.tsx
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
});
useEffect(() => {
if (loaded) {
SplashScreen.hide();
}
}, [loaded]);
if (!loaded) {
return null;
}
return <Stack />;
}
// app/products/_layout.tsx
import { Stack } from "expo-router";
export const unstable_settings = {
initialRouteName: "index",
};
export default function ProductsLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: "Products" }} />
<Stack.Screen name="[productId]" options={{ headerShown: false }} />
</Stack>
);
}
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
export default function TabLayout() {
return (
<Tabs screenOptions={{ headerShown: false }}>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color }) => (
<MaterialIcons size={28} name="home" color={color} />
),
}}
/>
<Tabs.Screen name="feed" options={{ title: "Feed" }} />
<Tabs.Screen name="settings" options={{ title: "Settings" }} />
</Tabs>
);
}
// app/_layout.tsx
import { Stack } from "expo-router";
import { useAuthState } from "@/hooks/useAuthState";
export default function RootLayout() {
const { isLoggedIn } = useAuthState();
return (
<Stack>
<Stack.Protected guard={isLoggedIn}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
</Stack.Protected>
<Stack.Protected guard={!isLoggedIn}>
<Stack.Screen name="sign-in" />
<Stack.Screen name="create-account" />
</Stack.Protected>
</Stack>
);
}
import { Link } from "expo-router";
// Basic link
<Link href="/about">About</Link>
// With custom component
<Link href="/profile" asChild>
<Pressable>
<Text>Profile</Text>
</Pressable>
</Link>
// Dynamic route
<Link href={{ pathname: "/user/[id]", params: { id: "123" } }}>
View User
</Link>
// With prefetching
<Link href="/heavy-page" prefetch>Heavy Page</Link>
import { useRouter } from "expo-router";
export default function Component() {
const router = useRouter();
const handleNavigate = () => {
// Navigate (adds to history)
router.navigate("/about");
// Push (always adds to stack)
router.push("/details");
// Replace (no back navigation)
router.replace("/home");
// Back
router.back();
// Dynamic route
router.navigate({
pathname: "/user/[id]",
params: { id: "123" },
});
};
return <Button onPress={handleNavigate} title="Navigate" />;
}
Always validate parameters before navigation to prevent broken URLs.
const handleNavigation = useCallback(() => {
if (!entityId) {
console.error("Cannot navigate: entity ID is missing");
return;
}
router.push(`/players/${entityId}`);
}, [entityId, router]);
import { useLocalSearchParams, useGlobalSearchParams } from "expo-router";
export default function UserPage() {
// Local params (current route only)
const { id, tab } = useLocalSearchParams<{ id: string; tab?: string }>();
// Global params (entire URL)
const globalParams = useGlobalSearchParams();
return <Text>User ID: {id}</Text>;
}
In app.json or app.config.js:
{
"expo": {
"scheme": "myapp"
}
}
Ensure proper back navigation when deep linking.
// app/feed/_layout.tsx
export const unstable_settings = {
initialRouteName: "index",
};
export default function FeedLayout() {
return <Stack />;
}
// Forces initial route to load first
<Link href="/feed/post/123" withAnchor>
View Post
</Link>
app/
├── (tabs)/
│ ├── _layout.tsx # Tab navigator
│ ├── index.tsx # Home tab
│ ├── feed/ # Feed tab with stack
│ │ ├── _layout.tsx # Stack navigator
│ │ ├── index.tsx # Feed list
│ │ └── [postId].tsx # Post detail
│ └── settings.tsx # Settings tab
app/
├── (tabs)/
│ ├── _layout.tsx
│ ├── (feed)/ # Feed tab group
│ │ └── index.tsx
│ ├── (search)/ # Search tab group
│ │ └── index.tsx
│ └── (feed,search)/ # Shared between both
│ └── users/
│ └── [userId].tsx
// app/_layout.tsx
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{
presentation: "modal",
animation: "slide_from_bottom",
}}
/>
</Stack>
Route files should only import and render feature components.
Avoid nesting stacks within stacks unnecessarily. Use route groups instead.
Always set initialRouteName in stack layouts for proper deep link behavior.
Use typed routes or constants instead of string literals.
// AVOID
router.push("/players/123/compare");
// PREFER
router.push({
pathname: "/players/[playerId]/compare",
params: { playerId: "123" },
});
// AVOID
const width = window.innerWidth;
// PREFER
import { useWindowDimensions } from "react-native";
const { width } = useWindowDimensions();
For detailed documentation on specific topics, refer to:
references/official-docs.md - Condensed official Expo Router documentationscripts/generate-route.py - Route scaffolding scriptOfficial Documentation: https://docs.expo.dev/router/introduction/