Skip to content
Go back

Expo React Native TypeScript: Cross-Platform Mobile Development

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?

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 AppNavigatorsrc/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 HomeScreensrc/screens/HomeScreen.tsx

Best Practices Summary

  1. Type Safety: Use comprehensive TypeScript types throughout the application
  2. Navigation: Implement proper navigation with type-safe parameters
  3. State Management: Use Redux Toolkit with proper async handling
  4. Performance: Optimize FlatLists and implement proper memoization
  5. Offline Support: Handle network connectivity and cache data
  6. Error Handling: Implement comprehensive error boundaries and user feedback
  7. Accessibility: Add proper accessibility labels and navigation
  8. Testing: Write unit and integration tests for critical functionality
  9. Security: Implement secure storage and API communication
  10. 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!


Share this post on:

Previous Post
AWS Lambda TypeScript: Serverless Function Development
Next Post
React TypeScript Best Practices: Advanced Component Development