Expo React Native development patterns — project structure, Expo Router, Supabase integration, EAS Build, and component architecture. Auto-load when building features or setting up project infrastructure.
app/ # Expo Router (file-based routing)
_layout.tsx # Root layout (providers, fonts, splash)
(tabs)/ # Tab navigation group
_layout.tsx # Tab bar configuration
index.tsx # First tab (Home)
[tab-name].tsx # Other tabs
[feature]/ # Stack screens for features
index.tsx
[id].tsx # Dynamic routes
+not-found.tsx # 404 screen
src/
components/
ui/ # Base UI components (Button, Input, Card...)
[feature]/ # Feature-specific components
hooks/
useAnimated[X].ts # Animation hooks
useGesture[X].ts # Gesture hooks
use[Feature].ts # Feature hooks
features/
[domain]/ # Domain-specific (CLAUDE.md + tests)
lib/
supabase.ts # Supabase client singleton
api/ # API functions grouped by domain
styles/
colors.ts # Color palettes (light/dark)
spacing.ts # Spacing scale
typography.ts # Typography scales
layout.ts # Layout constants
DESIGN.md # Design system document
constants/
animations.ts # Spring configs, timing presets
types/
index.ts # Shared types
database.ts # Supabase generated types
// app/_layout.tsx
export default function RootLayout() {
const [fontsLoaded] = useFonts({ ... });
useEffect(() => {
if (fontsLoaded) SplashScreen.hideAsync();
}, [fontsLoaded]);
if (!fontsLoaded) return null;
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Stack screenOptions={{ headerShown: false }} />
</GestureHandlerRootView>
);
}
// app/(tabs)/_layout.tsx
export default function TabLayout() {
return (
<Tabs screenOptions={{
tabBarActiveTintColor: Colors.primary,
headerShown: false,
}}>
<Tabs.Screen name="index" options={{
title: 'Home',
tabBarIcon: ({ color }) => <IconSymbol name="house" color={color} />,
}} />
</Tabs>
);
}
import { useRouter, useLocalSearchParams } from 'expo-router';
const router = useRouter();
router.push('/profile/123');
router.back();
router.replace('/(tabs)');
const { id } = useLocalSearchParams<{ id: string }>();
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
import * as SecureStore from 'expo-secure-store';
import { Database } from '@/types/database';
const ExpoSecureStoreAdapter = {
getItem: (key: string) => SecureStore.getItemAsync(key),
setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
removeItem: (key: string) => SecureStore.deleteItemAsync(key),
};
import { env } from '@/lib/env';
export const supabase = createClient<Database>(
env.SUPABASE_URL!,
env.SUPABASE_ANON_KEY!,
{ auth: { storage: ExpoSecureStoreAdapter, autoRefreshToken: true } }
);
// hooks/useAuth.ts
export function useAuth() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
supabase.auth.getUser().then(({ data }) => setUser(data.user));
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_, session) => setUser(session?.user ?? null)
);
return () => subscription.unsubscribe();
}, []);
return { user, signIn, signOut };
}
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export function useItems() {
return useQuery({
queryKey: ['items'],
queryFn: async () => {
const { data, error } = await supabase
.from('items')
.select('*')
.order('created_at', { ascending: false });
if (error) throw error;
return data;
},
});
}
{
"cli": { "version": ">= 13.0.0" },
"build": {
"development": { "developmentClient": true, "distribution": "internal" },
"preview": { "distribution": "internal" },
"production": {}
},
"submit": {
"production": {
"ios": { "ascAppId": "APP_ID", "appleTeamId": "TEAM_ID" },
"android": { "serviceAccountKeyPath": "./google-service-account.json" }
}
}
}
eas build --platform ios --profile development # Dev client
eas build --platform ios --profile production # Store build
eas update --branch production --message "fix" # OTA update
interface Props {
// Explicit, typed props
}
export function Component({ ...props }: Props) {
// 1. Hooks (state, refs, animation values)
// 2. Derived values
// 3. Handlers
// 4. Render
return ( ... );
}
const styles = StyleSheet.create({ ... });
Rules:
Answer in Korean.