Scaffold a production-ready React Native 0.76+ app with Expo SDK 52+, Expo Router for file-based navigation, Zustand for state management, TypeScript, EAS Build, platform-specific code, and AsyncStorage.
npx expo)npm install -g eas-cli)npx create-expo-app@latest <project-name> --template tabs
cd <project-name>
# Install core dependencies
npx expo install expo-router expo-linking expo-constants expo-status-bar
npx expo install @react-native-async-storage/async-storage
npx expo install expo-secure-store
npx expo install expo-splash-screen expo-image
# State management
npm install zustand
# HTTP client
npm install axios
# Form handling
npm install react-hook-form zod @hookform/resolvers
# Dev dependencies
npm install -D @types/react @types/react-nativeapp/
_layout.tsx # Root layout — providers, fonts, splash screen
(tabs)/
_layout.tsx # Tab navigator layout
index.tsx # Home tab — @page "/"
profile.tsx # Profile tab
(auth)/
_layout.tsx # Auth flow layout (no tabs)
login.tsx # Login screen
register.tsx # Register screen
users/
[id].tsx # Dynamic route — /users/:id
index.tsx # User list — /users
+not-found.tsx # 404 screen
+html.tsx # Custom HTML wrapper (web only)
components/
ui/
Button.tsx # Reusable UI components
Input.tsx
Card.tsx
LoadingSpinner.tsx
UserCard.tsx # Domain-specific components
UserForm.tsx
hooks/
useAuth.ts # Authentication hook
useUsers.ts # Data fetching hook
stores/
authStore.ts # Zustand auth store
userStore.ts # Zustand user store
services/
api.ts # Axios instance with interceptors
auth.ts # Auth API calls
users.ts # User API calls
lib/
storage.ts # AsyncStorage wrapper
constants.ts # App constants
types.ts # Shared TypeScript types
assets/
images/
fonts/app/ defines navigation_layout.tsx) wrap child routes and define navigation structure (tabs, stacks, drawers)(group) create route groups without affecting the URL[param].tsx syntax, accessed via useLocalSearchParams().ios.tsx / .android.tsx suffixes or Platform.select()expo-secure-store for tokens/secrets, AsyncStorage for non-sensitive dataservices/ — components never call fetch/axios directlyany typesexpo-image instead of <Image> from React Native for better performance and cachingapp/_layout.tsximport { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { useEffect } from 'react';
import * as SplashScreen from 'expo-splash-screen';
import { useFonts } from 'expo-font';
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [fontsLoaded] = useFonts({
'Inter-Regular': require('../assets/fonts/Inter-Regular.ttf'),
'Inter-Bold': require('../assets/fonts/Inter-Bold.ttf'),
});
useEffect(() => {
if (fontsLoaded) {
SplashScreen.hideAsync();
}
}, [fontsLoaded]);
if (!fontsLoaded) return null;
return (
<>
<StatusBar style="auto" />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="(auth)" />
<Stack.Screen name="users/[id]" options={{ headerShown: true, title: 'User' }} />
<Stack.Screen name="+not-found" />
</Stack>
</>
);
}app/(tabs)/_layout.tsximport { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#007AFF',
headerShown: true,
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}app/users/index.tsximport { View, FlatList, StyleSheet, ActivityIndicator, Pressable } from 'react-native';
import { Link } from 'expo-router';
import { useUsers } from '../../hooks/useUsers';
import { UserCard } from '../../components/UserCard';
export default function UsersScreen() {
const { users, isLoading, error, refresh } = useUsers();
if (isLoading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" />
</View>
);
}
return (
<FlatList
data={users}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Link href={`/users/${item.id}`} asChild>
<Pressable>
<UserCard user={item} />
</Pressable>
</Link>
)}
onRefresh={refresh}
refreshing={isLoading}
contentContainerStyle={styles.list}
/>
);
}
const styles = StyleSheet.create({
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
list: { padding: 16, gap: 12 },
});app/users/[id].tsximport { View, Text, StyleSheet } from 'react-native';
import { useLocalSearchParams, Stack } from 'expo-router';
import { useEffect, useState } from 'react';
import { userService } from '../../services/users';
import type { User } from '../../lib/types';
export default function UserDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
userService.getById(id).then(setUser);
}, [id]);
if (!user) return null;
return (
<>
<Stack.Screen options={{ title: user.name }} />
<View style={styles.container}>
<Text style={styles.name}>{user.name}</Text>
<Text style={styles.email}>{user.email}</Text>
</View>
</>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
name: { fontSize: 24, fontWeight: 'bold' },
email: { fontSize: 16, color: '#666', marginTop: 4 },
});stores/authStore.tsimport { create } from 'zustand';
import * as SecureStore from 'expo-secure-store';
import { authService } from '../services/auth';
interface AuthState {
token: string | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
loadToken: () => Promise<void>;
}
export const useAuthStore = create<AuthState>((set) => ({
token: null,
isAuthenticated: false,
isLoading: true,
login: async (email, password) => {
const { token } = await authService.login(email, password);
await SecureStore.setItemAsync('auth_token', token);
set({ token, isAuthenticated: true });
},
logout: async () => {
await SecureStore.deleteItemAsync('auth_token');
set({ token: null, isAuthenticated: false });
},
loadToken: async () => {
const token = await SecureStore.getItemAsync('auth_token');
set({ token, isAuthenticated: !!token, isLoading: false });
},
}));services/api.tsimport axios from 'axios';
import * as SecureStore from 'expo-secure-store';
import Constants from 'expo-constants';
const API_URL = Constants.expoConfig?.extra?.apiUrl ?? 'http://localhost:8080';
export const api = axios.create({
baseURL: `${API_URL}/api`,
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
});
// Attach auth token to every request
api.interceptors.request.use(async (config) => {
const token = await SecureStore.getItemAsync('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Handle 401 globally
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
await SecureStore.deleteItemAsync('auth_token');
// Navigate to login — handled by auth state listener
}
return Promise.reject(error);
}
);services/users.tsimport { api } from './api';
import type { User, CreateUserRequest } from '../lib/types';
export const userService = {
getAll: async (): Promise<User[]> => {
const { data } = await api.get('/users');
return data.data;
},
getById: async (id: string): Promise<User> => {
const { data } = await api.get(`/users/${id}`);
return data.data;
},
create: async (body: CreateUserRequest): Promise<User> => {
const { data } = await api.post('/users', body);
return data.data;
},
update: async (id: string, body: Partial<CreateUserRequest>): Promise<User> => {
const { data } = await api.put(`/users/${id}`, body);
return data.data;
},
delete: async (id: string): Promise<void> => {
await api.delete(`/users/${id}`);
},
};hooks/useUsers.tsimport { useEffect, useState, useCallback } from 'react';
import { userService } from '../services/users';
import type { User } from '../lib/types';
export function useUsers() {
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetch = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const data = await userService.getAll();
setUsers(data);
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to fetch users');
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
fetch();
}, [fetch]);
return { users, isLoading, error, refresh: fetch };
}// components/ui/Button.tsx — shared
import { Platform, Pressable, Text, StyleSheet } from 'react-native';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary';
}
export function Button({ title, onPress, variant = 'primary' }: ButtonProps) {
return (
<Pressable
onPress={onPress}
style={({ pressed }) => [
styles.button,
variant === 'primary' ? styles.primary : styles.secondary,
pressed && styles.pressed,
// Platform-specific shadow
Platform.select({
ios: styles.iosShadow,
android: { elevation: 4 },
}),
]}
>
<Text style={[styles.text, variant === 'secondary' && styles.secondaryText]}>
{title}
</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: { paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8, alignItems: 'center' },
primary: { backgroundColor: '#007AFF' },
secondary: { backgroundColor: 'transparent', borderWidth: 1, borderColor: '#007AFF' },
pressed: { opacity: 0.7 },
text: { color: '#fff', fontSize: 16, fontWeight: '600' },
secondaryText: { color: '#007AFF' },
iosShadow: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 4 },
});lib/types.tsexport interface User {
id: string;
email: string;
name: string;
createdAt: string;
}
export interface CreateUserRequest {
email: string;
name: string;
password: string;
}
export interface AuthResponse {
token: string;
user: User;
}lib/storage.tsimport AsyncStorage from '@react-native-async-storage/async-storage';
export const storage = {
get: async <T>(key: string): Promise<T | null> => {
const value = await AsyncStorage.getItem(key);
return value ? JSON.parse(value) : null;
},
set: async <T>(key: string, value: T): Promise<void> => {
await AsyncStorage.setItem(key, JSON.stringify(value));
},
remove: async (key: string): Promise<void> => {
await AsyncStorage.removeItem(key);
},
clear: async (): Promise<void> => {
await AsyncStorage.clear();
},
};# Start development server
npx expo start
# Run on specific platform
npx expo run:ios
npx expo run:android
# Start with tunnel (for physical device on different network)
npx expo start --tunnel
# Clear cache
npx expo start --clear
# Install Expo-compatible package
npx expo install <package-name>
# EAS Build
eas build --platform ios
eas build --platform android
eas build --platform all
# EAS Submit to stores
eas submit --platform ios
eas submit --platform android
# EAS Update (OTA updates)
eas update --branch production --message "Bug fix"
# Prebuild native projects (if ejecting from managed workflow)
npx expo prebuild
# Lint
npx expo lint
# TypeScript check
npx tsc --noEmit<Link> for declarative navigation, router.push() / router.replace() for imperative.@tanstack/react-query alongside Zustand.(auth) and (tabs) with a root layout that redirects based on useAuthStore().isAuthenticated. Expo Router supports redirect in layout components.expo-secure-store for tokens and secrets (uses Keychain on iOS, Keystore on Android). AsyncStorage for non-sensitive data (preferences, cached data).eas build. Configure in eas.json with profiles (development, preview, production). Development builds include dev tools, production builds are store-ready.eas update pushes JS bundle updates without app store review. Configure update channels per build profile..ios.tsx / .android.tsx file suffixes for large platform differences. Use Platform.OS or Platform.select() for small differences.jest with @testing-library/react-native for component tests. Use detox for E2E tests on simulators/emulators.@react-native-firebase/app + feature packages (e.g., @react-native-firebase/auth, @react-native-firebase/firestore).achreftlili/deep-dev-skills
181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.