Mobile (Expo)
Mobile Architecture
Manacore mobile apps use Expo SDK 52+ with React Native, Expo Router, and NativeWind for styling.
Project Structure
apps/{project}/apps/mobile/├── app/ # Expo Router routes│ ├── _layout.tsx # Root layout│ ├── index.tsx # Home screen│ ├── (auth)/ # Auth screens│ │ ├── _layout.tsx│ │ ├── login.tsx│ │ └── register.tsx│ └── (tabs)/ # Tab navigation│ ├── _layout.tsx│ ├── index.tsx│ ├── settings.tsx│ └── profile.tsx├── src/│ ├── components/ # React components│ │ ├── ui/ # Base UI components│ │ └── features/ # Feature components│ ├── hooks/ # Custom hooks│ ├── stores/ # Zustand stores│ ├── services/ # API clients│ ├── utils/ # Helper functions│ └── types/ # TypeScript types├── assets/ # Images, fonts├── app.json # Expo config├── tailwind.config.js # NativeWind config└── package.jsonExpo Router
Root Layout
import { Stack } from 'expo-router';import { useEffect } from 'react';import { useAuth } from '../src/hooks/useAuth';import '../global.css'; // NativeWind styles
export default function RootLayout() { const { isLoading, isAuthenticated } = useAuth();
if (isLoading) { return <LoadingScreen />; }
return ( <Stack screenOptions={{ headerShown: false }}> <Stack.Screen name="(auth)" /> <Stack.Screen name="(tabs)" /> </Stack> );}Tab Navigation
// app/(tabs)/_layout.tsximport { Tabs } from 'expo-router';import { Home, Settings, User } from 'lucide-react-native';
export default function TabLayout() { return ( <Tabs screenOptions={{ tabBarActiveTintColor: '#3b82f6', headerShown: false, }} > <Tabs.Screen name="index" options={{ title: 'Home', tabBarIcon: ({ color, size }) => <Home color={color} size={size} />, }} /> <Tabs.Screen name="settings" options={{ title: 'Settings', tabBarIcon: ({ color, size }) => <Settings color={color} size={size} />, }} /> <Tabs.Screen name="profile" options={{ title: 'Profile', tabBarIcon: ({ color, size }) => <User color={color} size={size} />, }} /> </Tabs> );}Protected Routes
// app/(tabs)/_layout.tsximport { Redirect, Tabs } from 'expo-router';import { useAuth } from '../../src/hooks/useAuth';
export default function TabLayout() { const { isAuthenticated } = useAuth();
if (!isAuthenticated) { return <Redirect href="/login" />; }
return ( <Tabs> {/* ... */} </Tabs> );}NativeWind (Tailwind)
Setup
module.exports = { content: [ './app/**/*.{js,jsx,ts,tsx}', './src/**/*.{js,jsx,ts,tsx}', ], presets: [require('nativewind/preset')], theme: { extend: {}, }, plugins: [],};Usage
import { View, Text, Pressable } from 'react-native';
export function Button({ title, onPress }) { return ( <Pressable className="bg-blue-500 px-4 py-2 rounded-lg active:bg-blue-600" onPress={onPress} > <Text className="text-white font-semibold text-center"> {title} </Text> </Pressable> );}State Management (Zustand)
import { create } from 'zustand';import { persist, createJSONStorage } from 'zustand/middleware';import AsyncStorage from '@react-native-async-storage/async-storage';
interface AuthState { user: User | null; token: string | null; setAuth: (user: User, token: string) => void; logout: () => void;}
export const useAuthStore = create<AuthState>()( persist( (set) => ({ user: null, token: null, setAuth: (user, token) => set({ user, token }), logout: () => set({ user: null, token: null }), }), { name: 'auth-storage', storage: createJSONStorage(() => AsyncStorage), } ));Usage in Components
import { useAuthStore } from '../stores/auth';
export function ProfileScreen() { const { user, logout } = useAuthStore();
return ( <View className="flex-1 p-4"> <Text className="text-xl font-bold">{user?.name}</Text> <Text className="text-gray-500">{user?.email}</Text>
<Pressable className="mt-4 bg-red-500 p-3 rounded-lg" onPress={logout} > <Text className="text-white text-center">Logout</Text> </Pressable> </View> );}API Integration
API Client
import { useAuthStore } from '../stores/auth';
const API_URL = process.env.EXPO_PUBLIC_API_URL;
class ApiClient { private async fetch<T>(path: string, options: RequestInit = {}): Promise<T> { const token = useAuthStore.getState().token;
const response = await fetch(`${API_URL}${path}`, { ...options, headers: { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }), ...options.headers, }, });
if (!response.ok) { if (response.status === 401) { useAuthStore.getState().logout(); } throw new Error(`API Error: ${response.status}`); }
return response.json(); }
// API methods getUser = () => this.fetch<User>('/api/v1/me'); getItems = () => this.fetch<Item[]>('/api/v1/items'); createItem = (data: CreateItemDto) => this.fetch<Item>('/api/v1/items', { method: 'POST', body: JSON.stringify(data), });}
export const api = new ApiClient();React Query Integration
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';import { api } from '../services/api';
export function useItems() { return useQuery({ queryKey: ['items'], queryFn: api.getItems, });}
export function useCreateItem() { const queryClient = useQueryClient();
return useMutation({ mutationFn: api.createItem, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['items'] }); }, });}Components
Base Component Pattern
import { View, ViewProps } from 'react-native';import { cn } from '../../utils/cn';
interface CardProps extends ViewProps { variant?: 'default' | 'elevated';}
export function Card({ variant = 'default', className, children, ...props}: CardProps) { return ( <View className={cn( 'bg-white rounded-xl p-4', variant === 'elevated' && 'shadow-lg', className )} {...props} > {children} </View> );}List Component
import { FlatList, View, Text, ActivityIndicator } from 'react-native';import { useItems } from '../../hooks/useItems';import { Card } from '../ui/Card';
export function ItemList() { const { data: items, isLoading, error } = useItems();
if (isLoading) { return <ActivityIndicator className="flex-1" />; }
if (error) { return ( <View className="flex-1 items-center justify-center"> <Text className="text-red-500">Error loading items</Text> </View> ); }
return ( <FlatList data={items} keyExtractor={(item) => item.id} renderItem={({ item }) => ( <Card className="mb-3"> <Text className="font-semibold">{item.title}</Text> <Text className="text-gray-500">{item.description}</Text> </Card> )} contentContainerClassName="p-4" /> );}Best Practices
Common Commands
# Start development serverpnpm dev
# Run on iOS simulatorpnpm ios
# Run on Android emulatorpnpm android
# Build development versionpnpm build:dev
# Build production versionpnpm build:prod