Expo React Native TypeScript: Cross-Platform Mobile Development
Expo provides a powerful framework for building cross-platform mobile applications with React Native and TypeScript. This comprehensive guide covers project setup, navigation, native APIs, state management, and deployment to app stores.
Why Choose Expo with React Native?
- Cross-Platform: Write once, run on iOS and Android
- TypeScript Support: Full type safety and better developer experience
- Managed Workflow: Simplified development and deployment process
- Native APIs: Easy access to device features and sensors
- Over-the-Air Updates: Deploy updates without app store approval
Step 1: Development Environment Setup
Set up a complete Expo React Native TypeScript development environment:
# Install Expo CLI globally
npm install -g @expo/cli
# Create new Expo project with TypeScript template
npx create-expo-app --template expo-template-blank-typescript MyMobileApp
cd MyMobileApp
# Install navigation dependencies
npx expo install @react-navigation/native @react-navigation/stack
npx expo install @react-navigation/bottom-tabs @react-navigation/drawer
npx expo install react-native-screens react-native-safe-area-context
npx expo install react-native-gesture-handler react-native-reanimated
# Install state management
npm install @reduxjs/toolkit react-redux
npm install @tanstack/react-query
# Install UI components and styling
npx expo install react-native-paper react-native-vector-icons
npx expo install react-native-svg expo-linear-gradient
# Install utility libraries
npm install lodash date-fns
npm install @types/lodash
# Install development dependencies
npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm install -D prettier eslint-config-prettier
npm install -D jest @types/jest react-test-renderer
Configure TypeScript for optimal React Native development:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@/components/*": ["src/components/*"],
"@/screens/*": ["src/screens/*"],
"@/navigation/*": ["src/navigation/*"],
"@/hooks/*": ["src/hooks/*"],
"@/utils/*": ["src/utils/*"],
"@/types/*": ["src/types/*"],
"@/store/*": ["src/store/*"],
"@/services/*": ["src/services/*"],
"@/assets/*": ["assets/*"]
}
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
tsconfig.json
Step 2: Type Definitions and Interfaces
Create comprehensive type definitions for mobile app development:
// Navigation types
import { StackNavigationProp } from "@react-navigation/stack";
import { BottomTabNavigationProp } from "@react-navigation/bottom-tabs";
import { DrawerNavigationProp } from "@react-navigation/drawer";
import { RouteProp } from "@react-navigation/native";
// Stack Navigator Type
export type RootStackParamList = {
Welcome: undefined;
Login: undefined;
Register: { email?: string };
Home: undefined;
Profile: { userId: string };
Settings: undefined;
ProductDetail: { productId: string; categoryId?: string };
Cart: undefined;
Checkout: { items: CartItem[] };
OrderConfirmation: { orderId: string };
};
// Tab Navigator Type
export type BottomTabParamList = {
HomeTab: undefined;
SearchTab: undefined;
CartTab: undefined;
ProfileTab: undefined;
};
// Drawer Navigator Type
export type DrawerParamList = {
Main: undefined;
Orders: undefined;
Wishlist: undefined;
Support: undefined;
About: undefined;
};
// Navigation Props
export type StackNavigationProps<T extends keyof RootStackParamList> = {
navigation: StackNavigationProp<RootStackParamList, T>;
route: RouteProp<RootStackParamList, T>;
};
export type TabNavigationProps<T extends keyof BottomTabParamList> = {
navigation: BottomTabNavigationProp<BottomTabParamList, T>;
route: RouteProp<BottomTabParamList, T>;
};
// User types
export interface User {
id: string;
email: string;
username: string;
firstName: string;
lastName: string;
avatar?: string;
phone?: string;
dateOfBirth?: string;
gender?: "male" | "female" | "other";
address?: Address;
preferences: UserPreferences;
createdAt: string;
updatedAt: string;
}
export interface Address {
id: string;
street: string;
city: string;
state: string;
zipCode: string;
country: string;
isDefault: boolean;
}
export interface UserPreferences {
theme: "light" | "dark" | "system";
language: string;
currency: string;
notifications: {
push: boolean;
email: boolean;
sms: boolean;
orderUpdates: boolean;
promotions: boolean;
};
privacy: {
shareLocation: boolean;
shareUsageData: boolean;
personalizedAds: boolean;
};
}
// Product types
export interface Product {
id: string;
name: string;
description: string;
price: number;
originalPrice?: number;
discount?: number;
currency: string;
category: Category;
images: ProductImage[];
variants?: ProductVariant[];
specifications: ProductSpecification[];
reviews: ProductReview[];
rating: number;
reviewCount: number;
availability: {
inStock: boolean;
quantity: number;
restockDate?: string;
};
shipping: {
freeShipping: boolean;
estimatedDays: number;
cost?: number;
};
seller: Seller;
tags: string[];
createdAt: string;
updatedAt: string;
}
export interface Category {
id: string;
name: string;
slug: string;
description?: string;
image?: string;
parentId?: string;
level: number;
isActive: boolean;
}
export interface ProductImage {
id: string;
url: string;
thumbnailUrl: string;
alt: string;
isPrimary: boolean;
order: number;
}
export interface ProductVariant {
id: string;
name: string;
value: string;
price?: number;
image?: string;
availability: {
inStock: boolean;
quantity: number;
};
}
export interface ProductSpecification {
name: string;
value: string;
group?: string;
}
export interface ProductReview {
id: string;
userId: string;
userName: string;
userAvatar?: string;
rating: number;
title?: string;
comment: string;
images?: string[];
helpful: number;
verified: boolean;
createdAt: string;
}
export interface Seller {
id: string;
name: string;
avatar?: string;
rating: number;
reviewCount: number;
isVerified: boolean;
}
// Cart types
export interface CartItem {
id: string;
product: Product;
variant?: ProductVariant;
quantity: number;
price: number;
totalPrice: number;
addedAt: string;
}
export interface Cart {
id: string;
userId: string;
items: CartItem[];
itemCount: number;
totalAmount: number;
currency: string;
updatedAt: string;
}
// Order types
export interface Order {
id: string;
orderNumber: string;
userId: string;
status: OrderStatus;
items: OrderItem[];
subtotal: number;
shipping: number;
tax: number;
discount: number;
total: number;
currency: string;
shippingAddress: Address;
billingAddress: Address;
paymentMethod: PaymentMethod;
tracking?: TrackingInfo;
estimatedDelivery?: string;
createdAt: string;
updatedAt: string;
}
export type OrderStatus =
| "pending"
| "confirmed"
| "processing"
| "shipped"
| "delivered"
| "cancelled"
| "refunded";
export interface OrderItem extends CartItem {
orderId: string;
}
export interface PaymentMethod {
id: string;
type: "credit_card" | "debit_card" | "paypal" | "apple_pay" | "google_pay";
last4?: string;
brand?: string;
expiryMonth?: number;
expiryYear?: number;
}
export interface TrackingInfo {
carrier: string;
trackingNumber: string;
status: string;
estimatedDelivery: string;
events: TrackingEvent[];
}
export interface TrackingEvent {
status: string;
description: string;
location?: string;
timestamp: string;
}
// API response types
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
message?: string;
errors?: Record<string, string[]>;
meta?: {
pagination?: PaginationInfo;
timestamp: string;
requestId: string;
};
}
export interface PaginationInfo {
page: number;
limit: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
}
// Error types
export interface AppError {
code: string;
message: string;
details?: any;
}
// Form types
export interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
export interface RegisterForm {
firstName: string;
lastName: string;
email: string;
password: string;
confirmPassword: string;
acceptTerms: boolean;
}
export interface ProfileUpdateForm {
firstName: string;
lastName: string;
phone?: string;
dateOfBirth?: string;
gender?: string;
}
// Theme types
export interface ThemeColors {
primary: string;
secondary: string;
background: string;
surface: string;
text: string;
textSecondary: string;
border: string;
error: string;
warning: string;
success: string;
info: string;
}
export interface Theme {
colors: ThemeColors;
spacing: {
xs: number;
sm: number;
md: number;
lg: number;
xl: number;
};
typography: {
fontSize: {
xs: number;
sm: number;
md: number;
lg: number;
xl: number;
xxl: number;
};
fontWeight: {
light: string;
normal: string;
medium: string;
semibold: string;
bold: string;
};
};
borderRadius: {
sm: number;
md: number;
lg: number;
xl: number;
};
}
// Utility types
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
src/types/index.ts
Step 3: Navigation Setup
Create a comprehensive navigation structure:
import React from 'react'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { createDrawerNavigator } from '@react-navigation/drawer'
import { useColorScheme } from 'react-native'
import { Ionicons } from '@expo/vector-icons'
// Import screens
import WelcomeScreen from '@/screens/WelcomeScreen'
import LoginScreen from '@/screens/LoginScreen'
import RegisterScreen from '@/screens/RegisterScreen'
import HomeScreen from '@/screens/HomeScreen'
import SearchScreen from '@/screens/SearchScreen'
import CartScreen from '@/screens/CartScreen'
import ProfileScreen from '@/screens/ProfileScreen'
import ProductDetailScreen from '@/screens/ProductDetailScreen'
import CheckoutScreen from '@/screens/CheckoutScreen'
import OrderConfirmationScreen from '@/screens/OrderConfirmationScreen'
import SettingsScreen from '@/screens/SettingsScreen'
import OrdersScreen from '@/screens/OrdersScreen'
import WishlistScreen from '@/screens/WishlistScreen'
// Import types
import { RootStackParamList, BottomTabParamList, DrawerParamList } from '@/types'
// Import hooks
import { useAppSelector } from '@/hooks/redux'
// Import theme
import { lightTheme, darkTheme } from '@/utils/theme'
const Stack = createStackNavigator<RootStackParamList>()
const Tab = createBottomTabNavigator<BottomTabParamList>()
const Drawer = createDrawerNavigator<DrawerParamList>()
// Tab Navigator
function TabNavigator() {
const colorScheme = useColorScheme()
const theme = colorScheme === 'dark' ? darkTheme : lightTheme
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName: keyof typeof Ionicons.glyphMap
switch (route.name) {
case 'HomeTab':
iconName = focused ? 'home' : 'home-outline'
break
case 'SearchTab':
iconName = focused ? 'search' : 'search-outline'
break
case 'CartTab':
iconName = focused ? 'bag' : 'bag-outline'
break
case 'ProfileTab':
iconName = focused ? 'person' : 'person-outline'
break
default:
iconName = 'circle'
}
return <Ionicons name={iconName} size={size} color={color} />
},
tabBarActiveTintColor: theme.colors.primary,
tabBarInactiveTintColor: theme.colors.textSecondary,
tabBarStyle: {
backgroundColor: theme.colors.surface,
borderTopColor: theme.colors.border,
},
headerStyle: {
backgroundColor: theme.colors.primary,
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
})}
>
<Tab.Screen
name="HomeTab"
component={HomeScreen}
options={{
title: 'Home',
headerTitle: 'MyStore'
}}
/>
<Tab.Screen
name="SearchTab"
component={SearchScreen}
options={{ title: 'Search' }}
/>
<Tab.Screen
name="CartTab"
component={CartScreen}
options={{
title: 'Cart',
tabBarBadge: undefined, // Will be set by cart items count
}}
/>
<Tab.Screen
name="ProfileTab"
component={ProfileScreen}
options={{ title: 'Profile' }}
/>
</Tab.Navigator>
)
}
// Drawer Navigator
function DrawerNavigator() {
const colorScheme = useColorScheme()
const theme = colorScheme === 'dark' ? darkTheme : lightTheme
return (
<Drawer.Navigator
screenOptions={{
drawerStyle: {
backgroundColor: theme.colors.surface,
},
drawerActiveTintColor: theme.colors.primary,
drawerInactiveTintColor: theme.colors.textSecondary,
headerStyle: {
backgroundColor: theme.colors.primary,
},
headerTintColor: '#fff',
}}
>
<Drawer.Screen
name="Main"
component={TabNavigator}
options={{
title: 'Home',
drawerIcon: ({ color, size }) => (
<Ionicons name="home-outline" color={color} size={size} />
),
headerShown: false,
}}
/>
<Drawer.Screen
name="Orders"
component={OrdersScreen}
options={{
title: 'My Orders',
drawerIcon: ({ color, size }) => (
<Ionicons name="receipt-outline" color={color} size={size} />
),
}}
/>
<Drawer.Screen
name="Wishlist"
component={WishlistScreen}
options={{
title: 'Wishlist',
drawerIcon: ({ color, size }) => (
<Ionicons name="heart-outline" color={color} size={size} />
),
}}
/>
</Drawer.Navigator>
)
}
// Auth Stack Navigator
function AuthNavigator() {
const colorScheme = useColorScheme()
const theme = colorScheme === 'dark' ? darkTheme : lightTheme
return (
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: theme.colors.primary,
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
cardStyle: {
backgroundColor: theme.colors.background,
},
}}
>
<Stack.Screen
name="Welcome"
component={WelcomeScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Login"
component={LoginScreen}
options={{ title: 'Sign In' }}
/>
<Stack.Screen
name="Register"
component={RegisterScreen}
options={{ title: 'Create Account' }}
/>
</Stack.Navigator>
)
}
// Main App Navigator
function AppNavigator() {
const colorScheme = useColorScheme()
const theme = colorScheme === 'dark' ? darkTheme : lightTheme
const { isAuthenticated } = useAppSelector(state => state.auth)
return (
<NavigationContainer
theme={{
dark: colorScheme === 'dark',
colors: {
primary: theme.colors.primary,
background: theme.colors.background,
card: theme.colors.surface,
text: theme.colors.text,
border: theme.colors.border,
notification: theme.colors.error,
},
}}
>
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
<>
{/* Main App Flow */}
<Stack.Screen name="Home" component={DrawerNavigator} />
<Stack.Screen
name="ProductDetail"
component={ProductDetailScreen}
options={{
headerShown: true,
title: 'Product Details',
headerStyle: {
backgroundColor: theme.colors.primary,
},
headerTintColor: '#fff',
}}
/>
<Stack.Screen
name="Checkout"
component={CheckoutScreen}
options={{
headerShown: true,
title: 'Checkout',
headerStyle: {
backgroundColor: theme.colors.primary,
},
headerTintColor: '#fff',
}}
/>
<Stack.Screen
name="OrderConfirmation"
component={OrderConfirmationScreen}
options={{
headerShown: true,
title: 'Order Confirmed',
headerStyle: {
backgroundColor: theme.colors.success,
},
headerTintColor: '#fff',
headerLeft: () => null, // Prevent going back
}}
/>
<Stack.Screen
name="Settings"
component={SettingsScreen}
options={{
headerShown: true,
title: 'Settings',
headerStyle: {
backgroundColor: theme.colors.primary,
},
headerTintColor: '#fff',
}}
/>
</>
) : (
<>
{/* Auth Flow */}
<Stack.Screen name="Auth" component={AuthNavigator} />
</>
)}
</Stack.Navigator>
</NavigationContainer>
)
}
export default AppNavigator
src/navigation/AppNavigator.tsx
Step 4: Redux Store Setup
Create a comprehensive Redux store with TypeScript:
import { configureStore } from "@reduxjs/toolkit";
import { persistStore, persistReducer } from "redux-persist";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { combineReducers } from "@reduxjs/toolkit";
// Import slices
import authSlice from "./slices/authSlice";
import userSlice from "./slices/userSlice";
import cartSlice from "./slices/cartSlice";
import productsSlice from "./slices/productsSlice";
import ordersSlice from "./slices/ordersSlice";
import wishlistSlice from "./slices/wishlistSlice";
import settingsSlice from "./slices/settingsSlice";
// Persist config
const persistConfig = {
key: "root",
storage: AsyncStorage,
whitelist: ["auth", "cart", "settings", "wishlist"], // Only persist these reducers
blacklist: [], // Don't persist these reducers
};
// Root reducer
const rootReducer = combineReducers({
auth: authSlice,
user: userSlice,
cart: cartSlice,
products: productsSlice,
orders: ordersSlice,
wishlist: wishlistSlice,
settings: settingsSlice,
});
// Persisted reducer
const persistedReducer = persistReducer(persistConfig, rootReducer);
// Configure store
export const store = configureStore({
reducer: persistedReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ["persist/PERSIST", "persist/REHYDRATE"],
},
}),
devTools: __DEV__,
});
// Persistor
export const persistor = persistStore(store);
// Types
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
src/store/index.ts
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { User, LoginForm, RegisterForm, ApiResponse } from "@/types";
import { authService } from "@/services/authService";
interface AuthState {
isAuthenticated: boolean;
user: User | null;
token: string | null;
refreshToken: string | null;
loading: boolean;
error: string | null;
}
const initialState: AuthState = {
isAuthenticated: false,
user: null,
token: null,
refreshToken: null,
loading: false,
error: null,
};
// Async thunks
export const loginAsync = createAsyncThunk<
{ user: User; token: string; refreshToken: string },
LoginForm,
{ rejectValue: string }
>("auth/login", async (credentials, { rejectWithValue }) => {
try {
const response = await authService.login(credentials);
return response.data;
} catch (error: any) {
return rejectWithValue(error.message || "Login failed");
}
});
export const registerAsync = createAsyncThunk<
{ user: User; token: string; refreshToken: string },
RegisterForm,
{ rejectValue: string }
>("auth/register", async (userData, { rejectWithValue }) => {
try {
const response = await authService.register(userData);
return response.data;
} catch (error: any) {
return rejectWithValue(error.message || "Registration failed");
}
});
export const logoutAsync = createAsyncThunk<
void,
void,
{ rejectValue: string }
>("auth/logout", async (_, { rejectWithValue }) => {
try {
await authService.logout();
} catch (error: any) {
return rejectWithValue(error.message || "Logout failed");
}
});
export const refreshTokenAsync = createAsyncThunk<
{ token: string; refreshToken: string },
void,
{ rejectValue: string }
>("auth/refreshToken", async (_, { getState, rejectWithValue }) => {
try {
const state = getState() as { auth: AuthState };
if (!state.auth.refreshToken) {
throw new Error("No refresh token available");
}
const response = await authService.refreshToken(state.auth.refreshToken);
return response.data;
} catch (error: any) {
return rejectWithValue(error.message || "Token refresh failed");
}
});
// Auth slice
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
clearError: state => {
state.error = null;
},
updateUser: (state, action: PayloadAction<Partial<User>>) => {
if (state.user) {
state.user = { ...state.user, ...action.payload };
}
},
clearAuth: state => {
state.isAuthenticated = false;
state.user = null;
state.token = null;
state.refreshToken = null;
state.error = null;
},
},
extraReducers: builder => {
// Login
builder
.addCase(loginAsync.pending, state => {
state.loading = true;
state.error = null;
})
.addCase(loginAsync.fulfilled, (state, action) => {
state.loading = false;
state.isAuthenticated = true;
state.user = action.payload.user;
state.token = action.payload.token;
state.refreshToken = action.payload.refreshToken;
state.error = null;
})
.addCase(loginAsync.rejected, (state, action) => {
state.loading = false;
state.error = action.payload || "Login failed";
});
// Register
builder
.addCase(registerAsync.pending, state => {
state.loading = true;
state.error = null;
})
.addCase(registerAsync.fulfilled, (state, action) => {
state.loading = false;
state.isAuthenticated = true;
state.user = action.payload.user;
state.token = action.payload.token;
state.refreshToken = action.payload.refreshToken;
state.error = null;
})
.addCase(registerAsync.rejected, (state, action) => {
state.loading = false;
state.error = action.payload || "Registration failed";
});
// Logout
builder
.addCase(logoutAsync.pending, state => {
state.loading = true;
})
.addCase(logoutAsync.fulfilled, state => {
state.loading = false;
state.isAuthenticated = false;
state.user = null;
state.token = null;
state.refreshToken = null;
state.error = null;
})
.addCase(logoutAsync.rejected, (state, action) => {
state.loading = false;
// Still clear auth data even if logout API fails
state.isAuthenticated = false;
state.user = null;
state.token = null;
state.refreshToken = null;
state.error = action.payload || "Logout failed";
});
// Refresh Token
builder
.addCase(refreshTokenAsync.pending, state => {
state.loading = true;
})
.addCase(refreshTokenAsync.fulfilled, (state, action) => {
state.loading = false;
state.token = action.payload.token;
state.refreshToken = action.payload.refreshToken;
state.error = null;
})
.addCase(refreshTokenAsync.rejected, (state, action) => {
state.loading = false;
state.error = action.payload || "Token refresh failed";
// Clear auth data if refresh fails
state.isAuthenticated = false;
state.user = null;
state.token = null;
state.refreshToken = null;
});
},
});
export const { clearError, updateUser, clearAuth } = authSlice.actions;
export default authSlice.reducer;
src/store/slices/authSlice.ts
Step 5: Custom Hooks
Create powerful custom hooks for mobile development:
import { useState, useEffect, useCallback } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
interface UseAsyncStorageReturn<T> {
value: T | null;
loading: boolean;
error: Error | null;
setValue: (value: T) => Promise<void>;
removeValue: () => Promise<void>;
}
export function useAsyncStorage<T = string>(
key: string,
defaultValue: T | null = null
): UseAsyncStorageReturn<T> {
const [value, setValue] = useState<T | null>(defaultValue);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
// Load value from AsyncStorage
const loadValue = useCallback(async () => {
try {
setLoading(true);
setError(null);
const stored = await AsyncStorage.getItem(key);
if (stored !== null) {
const parsedValue = JSON.parse(stored);
setValue(parsedValue);
} else {
setValue(defaultValue);
}
} catch (err) {
setError(err instanceof Error ? err : new Error("Failed to load value"));
setValue(defaultValue);
} finally {
setLoading(false);
}
}, [key, defaultValue]);
// Save value to AsyncStorage
const saveValue = useCallback(
async (newValue: T) => {
try {
setError(null);
await AsyncStorage.setItem(key, JSON.stringify(newValue));
setValue(newValue);
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to save value")
);
throw err;
}
},
[key]
);
// Remove value from AsyncStorage
const removeValue = useCallback(async () => {
try {
setError(null);
await AsyncStorage.removeItem(key);
setValue(defaultValue);
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to remove value")
);
throw err;
}
}, [key, defaultValue]);
// Load initial value
useEffect(() => {
loadValue();
}, [loadValue]);
return {
value,
loading,
error,
setValue: saveValue,
removeValue,
};
}
src/hooks/useAsyncStorage.ts
import { useState, useEffect } from "react";
import { Dimensions, Platform } from "react-native";
import * as Device from "expo-device";
import Constants from "expo-constants";
interface DeviceInfo {
deviceType: "phone" | "tablet" | "tv" | "desktop" | "unknown";
platform: "ios" | "android" | "web";
screenWidth: number;
screenHeight: number;
isLandscape: boolean;
statusBarHeight: number;
deviceName?: string;
systemVersion?: string;
appVersion: string;
buildNumber?: string;
}
export function useDeviceInfo(): DeviceInfo {
const [dimensions, setDimensions] = useState(Dimensions.get("window"));
useEffect(() => {
const subscription = Dimensions.addEventListener("change", ({ window }) => {
setDimensions(window);
});
return () => subscription?.remove();
}, []);
const deviceType =
Device.deviceType === Device.DeviceType.PHONE
? "phone"
: Device.deviceType === Device.DeviceType.TABLET
? "tablet"
: Device.deviceType === Device.DeviceType.TV
? "tv"
: Device.deviceType === Device.DeviceType.DESKTOP
? "desktop"
: "unknown";
const platform =
Platform.OS === "ios"
? "ios"
: Platform.OS === "android"
? "android"
: "web";
return {
deviceType,
platform,
screenWidth: dimensions.width,
screenHeight: dimensions.height,
isLandscape: dimensions.width > dimensions.height,
statusBarHeight: Constants.statusBarHeight,
deviceName: Device.deviceName || undefined,
systemVersion: Device.osVersion || undefined,
appVersion: Constants.expoConfig?.version || "1.0.0",
buildNumber:
Constants.expoConfig?.ios?.buildNumber ||
Constants.expoConfig?.android?.versionCode?.toString(),
};
}
src/hooks/useDeviceInfo.ts
Step 6: Screen Components
Create comprehensive screen components:
import React, { useEffect, useState } from 'react'
import {
View,
Text,
StyleSheet,
FlatList,
RefreshControl,
TouchableOpacity,
Image,
Alert,
} from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Ionicons } from '@expo/vector-icons'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
// Import types
import { RootStackParamList, Product, Category } from '@/types'
// Import hooks
import { useAppDispatch, useAppSelector } from '@/hooks/redux'
import { useDeviceInfo } from '@/hooks/useDeviceInfo'
// Import store actions
import { fetchFeaturedProducts, fetchCategories } from '@/store/slices/productsSlice'
import { addToCart } from '@/store/slices/cartSlice'
// Import components
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { ErrorMessage } from '@/components/ErrorMessage'
import { ProductCard } from '@/components/ProductCard'
import { CategoryCard } from '@/components/CategoryCard'
// Import utils
import { formatCurrency } from '@/utils/formatters'
type HomeScreenNavigationProp = StackNavigationProp<RootStackParamList, 'Home'>
const HomeScreen: React.FC = () => {
const navigation = useNavigation<HomeScreenNavigationProp>()
const dispatch = useAppDispatch()
const deviceInfo = useDeviceInfo()
const {
featuredProducts,
categories,
loading,
error
} = useAppSelector(state => state.products)
const { user } = useAppSelector(state => state.auth)
const [refreshing, setRefreshing] = useState(false)
// Load initial data
useEffect(() => {
loadData()
}, [])
const loadData = async () => {
try {
await Promise.all([
dispatch(fetchFeaturedProducts()).unwrap(),
dispatch(fetchCategories()).unwrap(),
])
} catch (error) {
console.error('Failed to load home data:', error)
}
}
const handleRefresh = async () => {
setRefreshing(true)
await loadData()
setRefreshing(false)
}
const handleProductPress = (product: Product) => {
navigation.navigate('ProductDetail', {
productId: product.id,
categoryId: product.category.id
})
}
const handleAddToCart = async (product: Product) => {
try {
await dispatch(addToCart({
product,
quantity: 1,
})).unwrap()
Alert.alert(
'Added to Cart',
`${product.name} has been added to your cart.`,
[
{ text: 'Continue Shopping', style: 'cancel' },
{ text: 'View Cart', onPress: () => navigation.navigate('Cart') },
]
)
} catch (error) {
Alert.alert('Error', 'Failed to add item to cart. Please try again.')
}
}
const handleCategoryPress = (category: Category) => {
// Navigate to category products (you would implement this)
console.log('Navigate to category:', category.name)
}
const renderHeader = () => (
<View style={styles.header}>
<View style={styles.greeting}>
<Text style={styles.greetingText}>
Hello, {user?.firstName || 'Guest'}!
</Text>
<Text style={styles.subGreeting}>
What are you looking for today?
</Text>
</View>
<TouchableOpacity style={styles.notificationButton}>
<Ionicons name="notifications-outline" size={24} color="#333" />
</TouchableOpacity>
</View>
)
const renderSearchBar = () => (
<TouchableOpacity
style={styles.searchBar}
onPress={() => navigation.navigate('Search')}
>
<Ionicons name="search" size={20} color="#666" />
<Text style={styles.searchPlaceholder}>Search products...</Text>
<Ionicons name="filter" size={20} color="#666" />
</TouchableOpacity>
)
const renderCategories = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Categories</Text>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={categories}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<CategoryCard
category={item}
onPress={() => handleCategoryPress(item)}
/>
)}
contentContainerStyle={styles.categoriesList}
/>
</View>
)
const renderFeaturedProducts = () => (
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Featured Products</Text>
<TouchableOpacity>
<Text style={styles.seeAllText}>See All</Text>
</TouchableOpacity>
</View>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={featuredProducts}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<ProductCard
product={item}
onPress={() => handleProductPress(item)}
onAddToCart={() => handleAddToCart(item)}
style={styles.productCard}
/>
)}
contentContainerStyle={styles.productsList}
/>
</View>
)
if (loading && !featuredProducts.length) {
return <LoadingSpinner />
}
if (error && !featuredProducts.length) {
return (
<ErrorMessage
message={error}
onRetry={loadData}
/>
)
}
return (
<SafeAreaView style={styles.container}>
<FlatList
data={[]} // Empty data as we're using ListHeaderComponent
renderItem={null}
ListHeaderComponent={(
<>
{renderHeader()}
{renderSearchBar()}
{renderCategories()}
{renderFeaturedProducts()}
</>
)}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
colors={['#007AFF']}
tintColor="#007AFF"
/>
}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
/>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
scrollContent: {
paddingBottom: 20,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 15,
},
greeting: {
flex: 1,
},
greetingText: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
},
subGreeting: {
fontSize: 16,
color: '#666',
marginTop: 4,
},
notificationButton: {
padding: 8,
},
searchBar: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f5f5f5',
marginHorizontal: 20,
marginBottom: 20,
paddingHorizontal: 15,
paddingVertical: 12,
borderRadius: 10,
},
searchPlaceholder: {
flex: 1,
marginLeft: 10,
fontSize: 16,
color: '#666',
},
section: {
marginBottom: 25,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
marginBottom: 15,
},
sectionTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#333',
},
seeAllText: {
fontSize: 16,
color: '#007AFF',
fontWeight: '600',
},
categoriesList: {
paddingLeft: 20,
},
productsList: {
paddingLeft: 20,
},
productCard: {
marginRight: 15,
},
})
export default HomeScreen
src/screens/HomeScreen.tsx
Best Practices Summary
- Type Safety: Use comprehensive TypeScript types throughout the application
- Navigation: Implement proper navigation with type-safe parameters
- State Management: Use Redux Toolkit with proper async handling
- Performance: Optimize FlatLists and implement proper memoization
- Offline Support: Handle network connectivity and cache data
- Error Handling: Implement comprehensive error boundaries and user feedback
- Accessibility: Add proper accessibility labels and navigation
- Testing: Write unit and integration tests for critical functionality
- Security: Implement secure storage and API communication
- App Store Optimization: Follow platform-specific guidelines and requirements
Development Commands
# Start development server
npx expo start
# Build for iOS
npx expo build:ios
# Build for Android
npx expo build:android
# Run on iOS simulator
npx expo run:ios
# Run on Android emulator
npx expo run:android
# Test on device
npx expo start --tunnel
Your Expo React Native TypeScript application is now ready for cross-platform mobile development with comprehensive navigation, state management, and production-ready features!