✨ Complete Guide to Adding New Features

Index

  1. General Process
  2. Add a New Screen
  3. Add a New Tab
  4. Add a Modal
  5. Add a New Component
  6. Add Global State
  7. Add a New Feature (Full Example)
  8. Integrate External APIs
  9. Add Push Notifications
  10. Add Analytics
  11. New Features Checklist

General Process

To add any new functionality, follow these steps:

1. Define types        → types/index.ts
2. Create mocks       → mocks/data.ts
3. Create components  → components/
4. Add context        → contexts/ (if you need global state)
5. Create screens     → app/
6. Configure navigation → app/_layout.tsx or app/(tabs)/_layout.tsx
7. Test               → iOS, Android, Web

Add a New Screen

Simple Screen (inside an existing Tab)

// app/(tabs)/explore/product-detail.tsx
import { View, Text, StyleSheet, ScrollView, Image } from 'react-native';
import { useLocalSearchParams, Stack } from 'expo-router';
import { useTheme } from '@/contexts/ThemeContext';
import { Button } from '@/components/Button';
import { Spacing, FontSizes } from '@/constants/colors';

export default function ProductDetailScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const { colors } = useTheme();

  return (
    <ScrollView style={[styles.container, { backgroundColor: colors.background }]}>
      <Stack.Screen 
        options={{ 
          title: 'Product Detail',
          headerStyle: { backgroundColor: colors.surface },
          headerTintColor: colors.text,
        }} 
      />
      
      <Image 
        source={{ uri: 'https://picsum.photos/400/300' }}
        style={styles.image}
      />
      
      <View style={styles.content}>
        <Text style={[styles.title, { color: colors.text }]}>
          Producto #{id}
        </Text>
        <Text style={[styles.description, { color: colors.textSecondary }]}>
          Product description here...
        </Text>
        
        <Button 
          title="Add to cart"
          onPress={() => console.log('Add to cart')}
          variant="primary"
        />
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  image: {
    width: '100%',
    height: 300,
  },
  content: {
    padding: Spacing.lg,
    gap: Spacing.md,
  },
  title: {
    fontSize: FontSizes.xxl,
    fontWeight: '700',
  },
  description: {
    fontSize: FontSizes.md,
    lineHeight: 24,
  },
});
// From any component
import { useRouter } from 'expo-router';

function ProductCard({ product }) {
  const router = useRouter();

  return (
    <TouchableOpacity 
      onPress={() => router.push({
        pathname: '/(tabs)/explore/product-detail',
        params: { id: product.id }
      })}
    >
      {/* content */}
    </TouchableOpacity>
  );
}

Add a New Tab

1. Create the Folder Structure

app/(tabs)/
└── my-new-tab/
    ├── _layout.tsx      # Inner stack layout
    ├── index.tsx        # Tab main screen
    └── detail.tsx       # Detail screen (optional)

2. Create the Tab Layout

// app/(tabs)/mi-nuevo-tab/_layout.tsx
import { Stack } from 'expo-router';
import { useTheme } from '@/contexts/ThemeContext';

export default function MiNuevoTabLayout() {
  const { colors } = useTheme();

  return (
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: colors.surface },
        headerTintColor: colors.text,
        headerShadowVisible: false,
      }}
    >
      <Stack.Screen 
        name="index" 
        options={{ title: 'Mi Nuevo Tab' }} 
      />
      <Stack.Screen 
        name="detalle" 
        options={{ title: 'Detalle' }} 
      />
    </Stack>
  );
}

3. Create the Main Screen

// app/(tabs)/mi-nuevo-tab/index.tsx
import { View, Text, StyleSheet, FlatList } from 'react-native';
import { useTheme } from '@/contexts/ThemeContext';
import { useI18n } from '@/contexts/I18nContext';
import { Card } from '@/components/Card';
import { EmptyState } from '@/components/EmptyState';

export default function MiNuevoTabScreen() {
  const { colors } = useTheme();
  const { t } = useI18n();
  
  const items = []; // Tu data aquí

  if (items.length === 0) {
    return (
      <View style={[styles.container, { backgroundColor: colors.background }]}>
        <EmptyState
          icon="Package"
          title="No items"
          description="Add your first item to get started"
          actionTitle="Add"
          onAction={() => {}}
        />
      </View>
    );
  }

  return (
    <View style={[styles.container, { backgroundColor: colors.background }]}>
      <FlatList
        data={items}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <Card item={item} onPress={() => {}} />
        )}
        contentContainerStyle={styles.list}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  list: {
    padding: 16,
    gap: 12,
  },
});

4. Register the Tab in the Layout

// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Package } from 'lucide-react-native'; // Importar icono

export default function TabLayout() {
  const { colors } = useTheme();

  return (
    <Tabs
      screenOptions={{
        headerShown: false,
        tabBarStyle: { backgroundColor: colors.surface },
        tabBarActiveTintColor: colors.primary,
        tabBarInactiveTintColor: colors.textTertiary,
      }}
    >
      {/* Tabs existentes */}
      <Tabs.Screen name="(home)" options={{ /* ... */ }} />
      <Tabs.Screen name="explore" options={{ /* ... */ }} />
      
      {/* Nuevo Tab */}
      <Tabs.Screen
        name="mi-nuevo-tab"
        options={{
          title: 'Mi Tab',
          tabBarIcon: ({ color, size }) => (
            <Package size={size} color={color} />
          ),
        }}
      />
      
      {/* Más tabs */}
      <Tabs.Screen name="profile" options={{ /* ... */ }} />
    </Tabs>
  );
}

Add a Modal

1. Create the Modal Screen

// app/crear-item.tsx (fuera de (tabs)/)
import { View, Text, StyleSheet, KeyboardAvoidingView, Platform } from 'react-native';
import { useRouter, Stack } from 'expo-router';
import { useState } from 'react';
import { useTheme } from '@/contexts/ThemeContext';
import { Input } from '@/components/Input';
import { Button } from '@/components/Button';
import { useToast } from '@/contexts/ToastContext';
import { X } from 'lucide-react-native';
import { TouchableOpacity } from 'react-native';

export default function CrearItemModal() {
  const router = useRouter();
  const { colors } = useTheme();
  const { showToast } = useToast();
  
  const [titulo, setTitulo] = useState('');
  const [descripcion, setDescripcion] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async () => {
    if (!titulo.trim()) {
      showToast('Title is required', 'error');
      return;
    }

    setIsLoading(true);
    try {
      // Lógica para crear el item
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      showToast('Item created successfully', 'success');
      router.back();
    } catch (error) {
      showToast('Error creating item', 'error');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <KeyboardAvoidingView 
      style={[styles.container, { backgroundColor: colors.background }]}
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
    >
      <Stack.Screen
        options={{
          presentation: 'modal',
          title: 'Create Item',
          headerStyle: { backgroundColor: colors.surface },
          headerTintColor: colors.text,
          headerLeft: () => (
            <TouchableOpacity onPress={() => router.back()}>
              <X size={24} color={colors.text} />
            </TouchableOpacity>
          ),
        }}
      />

      <View style={styles.content}>
        <Input
          label="Title"
          value={titulo}
          onChangeText={setTitulo}
          placeholder="Enter title..."
        />
        
        <Input
          label="Description"
          value={descripcion}
          onChangeText={setDescripcion}
          placeholder="Optional description..."
          multiline
          numberOfLines={4}
        />

        <Button
          title="Create"
          onPress={handleSubmit}
          loading={isLoading}
          disabled={isLoading}
        />
      </View>
    </KeyboardAvoidingView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  content: {
    padding: 16,
    gap: 16,
  },
});

2. Register in the Root Layout

// app/_layout.tsx
<Stack>
  <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
  <Stack.Screen 
    name="crear-item" 
    options={{ 
      presentation: 'modal',
      headerShown: true,
    }} 
  />
</Stack>

3. Open the Modal

// From anywhere
const router = useRouter();
router.push('/crear-item');

Add a New Component

Full Component Template

// components/ProductCard.tsx
import React, { useCallback } from 'react';
import { 
  View, 
  Text, 
  StyleSheet, 
  TouchableOpacity, 
  Image,
  ViewStyle,
} from 'react-native';
import { Heart, Star, ShoppingCart } from 'lucide-react-native';
import { useTheme } from '@/contexts/ThemeContext';
import { Colors, Spacing, FontSizes, BorderRadius } from '@/constants/colors';
import { triggerHaptic } from '@/utils/helpers';

// 1. Definir interface de Props
interface ProductCardProps {
  product: {
    id: string;
    name: string;
    price: number;
    image: string;
    rating: number;
    reviewCount: number;
  };
  isFavorite?: boolean;
  onPress?: () => void;
  onFavorite?: () => void;
  onAddToCart?: () => void;
  variant?: 'default' | 'compact' | 'horizontal';
  style?: ViewStyle;
  testID?: string;
}

// 2. Componente con props desestructuradas
export function ProductCard({
  product,
  isFavorite = false,
  onPress,
  onFavorite,
  onAddToCart,
  variant = 'default',
  style,
  testID,
}: ProductCardProps) {
  const { colors, isDark } = useTheme();

  // 3. Handlers con haptics
  const handleFavorite = useCallback(() => {
    triggerHaptic('light');
    onFavorite?.();
  }, [onFavorite]);

  const handleAddToCart = useCallback(() => {
    triggerHaptic('medium');
    onAddToCart?.();
  }, [onAddToCart]);

  // 4. Formatear precio
  const formattedPrice = new Intl.NumberFormat('es-ES', {
    style: 'currency',
    currency: 'EUR',
  }).format(product.price);

  // 5. Render condicional por variante
  if (variant === 'compact') {
    return (
      <TouchableOpacity
        style={[
          styles.compactContainer,
          { backgroundColor: colors.surface },
          style,
        ]}
        onPress={onPress}
        activeOpacity={0.7}
        testID={testID}
      >
        <Image source={{ uri: product.image }} style={styles.compactImage} />
        <Text style={[styles.compactName, { color: colors.text }]} numberOfLines={1}>
          {product.name}
        </Text>
        <Text style={[styles.compactPrice, { color: colors.primary }]}>
          {formattedPrice}
        </Text>
      </TouchableOpacity>
    );
  }

  if (variant === 'horizontal') {
    return (
      <TouchableOpacity
        style={[
          styles.horizontalContainer,
          { backgroundColor: colors.surface },
          style,
        ]}
        onPress={onPress}
        activeOpacity={0.7}
        testID={testID}
      >
        <Image source={{ uri: product.image }} style={styles.horizontalImage} />
        <View style={styles.horizontalContent}>
          <Text style={[styles.name, { color: colors.text }]} numberOfLines={2}>
            {product.name}
          </Text>
          <View style={styles.ratingRow}>
            <Star size={14} color={Colors.warning} fill={Colors.warning} />
            <Text style={[styles.rating, { color: colors.textSecondary }]}>
              {product.rating} ({product.reviewCount})
            </Text>
          </View>
          <Text style={[styles.price, { color: colors.primary }]}>
            {formattedPrice}
          </Text>
        </View>
        <TouchableOpacity onPress={handleFavorite} style={styles.favoriteButton}>
          <Heart 
            size={20} 
            color={isFavorite ? Colors.error : colors.textTertiary}
            fill={isFavorite ? Colors.error : 'transparent'}
          />
        </TouchableOpacity>
      </TouchableOpacity>
    );
  }

  // Default variant
  return (
    <TouchableOpacity
      style={[
        styles.container,
        { backgroundColor: colors.surface },
        style,
      ]}
      onPress={onPress}
      activeOpacity={0.7}
      testID={testID}
    >
      <View style={styles.imageContainer}>
        <Image source={{ uri: product.image }} style={styles.image} />
        <TouchableOpacity 
          style={[styles.favoriteOverlay, { backgroundColor: colors.surface }]}
          onPress={handleFavorite}
        >
          <Heart 
            size={18} 
            color={isFavorite ? Colors.error : colors.textTertiary}
            fill={isFavorite ? Colors.error : 'transparent'}
          />
        </TouchableOpacity>
      </View>

      <View style={styles.content}>
        <Text style={[styles.name, { color: colors.text }]} numberOfLines={2}>
          {product.name}
        </Text>
        
        <View style={styles.ratingRow}>
          <Star size={14} color={Colors.warning} fill={Colors.warning} />
          <Text style={[styles.rating, { color: colors.textSecondary }]}>
            {product.rating} ({product.reviewCount})
          </Text>
        </View>

        <View style={styles.footer}>
          <Text style={[styles.price, { color: colors.primary }]}>
            {formattedPrice}
          </Text>
          
          {onAddToCart && (
            <TouchableOpacity 
              style={[styles.cartButton, { backgroundColor: colors.primary }]}
              onPress={handleAddToCart}
            >
              <ShoppingCart size={16} color={Colors.textInverse} />
            </TouchableOpacity>
          )}
        </View>
      </View>
    </TouchableOpacity>
  );
}

// 6. Estilos con constantes
const styles = StyleSheet.create({
  // Default
  container: {
    borderRadius: BorderRadius.lg,
    overflow: 'hidden',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.06,
    shadowRadius: 8,
    elevation: 3,
  },
  imageContainer: {
    position: 'relative',
  },
  image: {
    width: '100%',
    height: 160,
  },
  favoriteOverlay: {
    position: 'absolute',
    top: Spacing.sm,
    right: Spacing.sm,
    padding: Spacing.xs,
    borderRadius: BorderRadius.full,
  },
  content: {
    padding: Spacing.md,
    gap: Spacing.xs,
  },
  name: {
    fontSize: FontSizes.md,
    fontWeight: '600' as const,
  },
  ratingRow: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: Spacing.xs,
  },
  rating: {
    fontSize: FontSizes.sm,
  },
  footer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    marginTop: Spacing.xs,
  },
  price: {
    fontSize: FontSizes.lg,
    fontWeight: '700' as const,
  },
  cartButton: {
    padding: Spacing.sm,
    borderRadius: BorderRadius.full,
  },
  favoriteButton: {
    padding: Spacing.sm,
  },

  // Horizontal
  horizontalContainer: {
    flexDirection: 'row',
    borderRadius: BorderRadius.lg,
    overflow: 'hidden',
    padding: Spacing.sm,
    gap: Spacing.md,
  },
  horizontalImage: {
    width: 80,
    height: 80,
    borderRadius: BorderRadius.md,
  },
  horizontalContent: {
    flex: 1,
    justifyContent: 'center',
    gap: Spacing.xs,
  },

  // Compact
  compactContainer: {
    width: 140,
    borderRadius: BorderRadius.lg,
    overflow: 'hidden',
  },
  compactImage: {
    width: '100%',
    height: 100,
  },
  compactName: {
    fontSize: FontSizes.sm,
    fontWeight: '500' as const,
    padding: Spacing.sm,
    paddingBottom: 0,
  },
  compactPrice: {
    fontSize: FontSizes.md,
    fontWeight: '700' as const,
    padding: Spacing.sm,
    paddingTop: Spacing.xs,
  },
});

Add Global State

Full Context with Persistence

// contexts/CartContext.tsx
import createContextHook from '@nkzw/create-context-hook';
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import AsyncStorage from '@react-native-async-storage/async-storage';

// 1. Definir tipos
interface CartItem {
  id: string;
  productId: string;
  name: string;
  price: number;
  quantity: number;
  image: string;
}

interface CartState {
  items: CartItem[];
  isLoading: boolean;
}

const STORAGE_KEY = 'shopping_cart';

// 2. Crear contexto
export const [CartProvider, useCart] = createContextHook(() => {
  const queryClient = useQueryClient();
  const [items, setItems] = useState<CartItem[]>([]);

  // Query para cargar del storage
  const cartQuery = useQuery({
    queryKey: ['cart'],
    queryFn: async (): Promise<CartItem[]> => {
      const stored = await AsyncStorage.getItem(STORAGE_KEY);
      return stored ? JSON.parse(stored) : [];
    },
  });

  // Sincronizar estado local con query
  useEffect(() => {
    if (cartQuery.data) {
      setItems(cartQuery.data);
    }
  }, [cartQuery.data]);

  // Mutation para guardar
  const saveMutation = useMutation({
    mutationFn: async (newItems: CartItem[]) => {
      await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newItems));
      return newItems;
    },
    onSuccess: (data) => {
      queryClient.setQueryData(['cart'], data);
    },
  });

  // Agregar item
  const addItem = useCallback((product: Omit<CartItem, 'id' | 'quantity'>) => {
    const existingIndex = items.findIndex(i => i.productId === product.productId);
    
    let newItems: CartItem[];
    if (existingIndex >= 0) {
      newItems = items.map((item, index) =>
        index === existingIndex
          ? { ...item, quantity: item.quantity + 1 }
          : item
      );
    } else {
      newItems = [
        ...items,
        {
          ...product,
          id: Date.now().toString(),
          quantity: 1,
        },
      ];
    }

    setItems(newItems);
    saveMutation.mutate(newItems);
  }, [items, saveMutation]);

  // Actualizar cantidad
  const updateQuantity = useCallback((itemId: string, quantity: number) => {
    if (quantity <= 0) {
      removeItem(itemId);
      return;
    }

    const newItems = items.map(item =>
      item.id === itemId ? { ...item, quantity } : item
    );
    setItems(newItems);
    saveMutation.mutate(newItems);
  }, [items, saveMutation]);

  // Remover item
  const removeItem = useCallback((itemId: string) => {
    const newItems = items.filter(item => item.id !== itemId);
    setItems(newItems);
    saveMutation.mutate(newItems);
  }, [items, saveMutation]);

  // Limpiar carrito
  const clearCart = useCallback(() => {
    setItems([]);
    saveMutation.mutate([]);
  }, [saveMutation]);

  // Cálculos derivados
  const totalItems = useMemo(() => 
    items.reduce((sum, item) => sum + item.quantity, 0),
    [items]
  );

  const totalPrice = useMemo(() =>
    items.reduce((sum, item) => sum + (item.price * item.quantity), 0),
    [items]
  );

  const isInCart = useCallback((productId: string) => 
    items.some(item => item.productId === productId),
    [items]
  );

  const getItemQuantity = useCallback((productId: string) => {
    const item = items.find(i => i.productId === productId);
    return item?.quantity ?? 0;
  }, [items]);

  return {
    items,
    isLoading: cartQuery.isLoading,
    
    // Acciones
    addItem,
    updateQuantity,
    removeItem,
    clearCart,
    
    // Helpers
    totalItems,
    totalPrice,
    isInCart,
    getItemQuantity,
  };
});

// 3. Hook derivado para item específico
export function useCartItem(productId: string) {
  const { items, updateQuantity, removeItem } = useCart();
  
  const item = useMemo(() => 
    items.find(i => i.productId === productId),
    [items, productId]
  );

  return {
    item,
    quantity: item?.quantity ?? 0,
    isInCart: !!item,
    increment: () => item && updateQuantity(item.id, item.quantity + 1),
    decrement: () => item && updateQuantity(item.id, item.quantity - 1),
    remove: () => item && removeItem(item.id),
  };
}

Use the Context

// In _layout.tsx
import { CartProvider } from '@/contexts/CartContext';

<QueryClientProvider client={queryClient}>
  <CartProvider>
    <AppProvider>
      <RootLayoutNav />
    </AppProvider>
  </CartProvider>
</QueryClientProvider>

// In components
function ProductActions({ product }) {
  const { addItem, isInCart, getItemQuantity } = useCart();
  const quantity = getItemQuantity(product.id);

  return (
    <View>
      {isInCart(product.id) ? (
        <Text>In cart: {quantity}</Text>
      ) : (
        <Button
          title="Add to cart"
          onPress={() => addItem({
            productId: product.id,
            name: product.name,
            price: product.price,
            image: product.image,
          })}
        />
      )}
    </View>
  );
}

Add a New Feature (Full Example)

Example: Reviews System

1. Definir Tipos

// types/index.ts
export interface Review {
  id: string;
  userId: string;
  userName: string;
  userAvatar?: string;
  productId: string;
  rating: number;
  title: string;
  content: string;
  images?: string[];
  helpful: number;
  createdAt: string;
}

2. Crear Mock Data

// mocks/reviews.ts
import { Review } from '@/types';

export const mockReviews: Review[] = [
  {
    id: '1',
    userId: 'user1',
    userName: 'María García',
    userAvatar: 'https://i.pravatar.cc/150?img=1',
    productId: 'prod1',
    rating: 5,
    title: 'Excelente producto',
    content: 'Superó mis expectativas. La calidad es increíble y llegó muy rápido.',
    images: ['https://picsum.photos/200/200?random=1'],
    helpful: 24,
    createdAt: '2024-01-15T10:00:00Z',
  },
  // ... más reseñas
];

3. Crear Componente

// components/ReviewCard.tsx
import React from 'react';
import { View, Text, StyleSheet, Image, TouchableOpacity } from 'react-native';
import { Star, ThumbsUp, MoreHorizontal } from 'lucide-react-native';
import { Review } from '@/types';
import { Avatar } from './Avatar';
import { useTheme } from '@/contexts/ThemeContext';
import { Spacing, FontSizes, BorderRadius, Colors } from '@/constants/colors';
import { formatRelativeTime, triggerHaptic } from '@/utils/helpers';

interface ReviewCardProps {
  review: Review;
  onHelpful?: () => void;
  onOptions?: () => void;
}

export function ReviewCard({ review, onHelpful, onOptions }: ReviewCardProps) {
  const { colors } = useTheme();

  const handleHelpful = () => {
    triggerHaptic('light');
    onHelpful?.();
  };

  return (
    <View style={[styles.container, { backgroundColor: colors.surface }]}>
      {/* Header */}
      <View style={styles.header}>
        <Avatar 
          source={review.userAvatar} 
          name={review.userName} 
          size="small" 
        />
        <View style={styles.headerContent}>
          <Text style={[styles.userName, { color: colors.text }]}>
            {review.userName}
          </Text>
          <Text style={[styles.date, { color: colors.textTertiary }]}>
            {formatRelativeTime(review.createdAt)}
          </Text>
        </View>
        {onOptions && (
          <TouchableOpacity onPress={onOptions}>
            <MoreHorizontal size={20} color={colors.textTertiary} />
          </TouchableOpacity>
        )}
      </View>

      {/* Rating */}
      <View style={styles.ratingRow}>
        {[1, 2, 3, 4, 5].map((star) => (
          <Star
            key={star}
            size={16}
            color={Colors.warning}
            fill={star <= review.rating ? Colors.warning : 'transparent'}
          />
        ))}
      </View>

      {/* Content */}
      <Text style={[styles.title, { color: colors.text }]}>
        {review.title}
      </Text>
      <Text style={[styles.content, { color: colors.textSecondary }]}>
        {review.content}
      </Text>

      {/* Images */}
      {review.images && review.images.length > 0 && (
        <View style={styles.imagesRow}>
          {review.images.map((image, index) => (
            <Image 
              key={index}
              source={{ uri: image }} 
              style={styles.reviewImage} 
            />
          ))}
        </View>
      )}

      {/* Footer */}
      <TouchableOpacity style={styles.helpfulButton} onPress={handleHelpful}>
        <ThumbsUp size={16} color={colors.textTertiary} />
        <Text style={[styles.helpfulText, { color: colors.textTertiary }]}>
          Útil ({review.helpful})
        </Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    padding: Spacing.md,
    borderRadius: BorderRadius.lg,
    gap: Spacing.sm,
  },
  header: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: Spacing.sm,
  },
  headerContent: {
    flex: 1,
  },
  userName: {
    fontSize: FontSizes.md,
    fontWeight: '600' as const,
  },
  date: {
    fontSize: FontSizes.xs,
  },
  ratingRow: {
    flexDirection: 'row',
    gap: 2,
  },
  title: {
    fontSize: FontSizes.md,
    fontWeight: '600' as const,
  },
  content: {
    fontSize: FontSizes.sm,
    lineHeight: 20,
  },
  imagesRow: {
    flexDirection: 'row',
    gap: Spacing.sm,
  },
  reviewImage: {
    width: 80,
    height: 80,
    borderRadius: BorderRadius.md,
  },
  helpfulButton: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: Spacing.xs,
    paddingTop: Spacing.sm,
  },
  helpfulText: {
    fontSize: FontSizes.sm,
  },
});

4. Crear Contexto

// contexts/ReviewsContext.tsx
import createContextHook from '@nkzw/create-context-hook';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Review } from '@/types';
import { mockReviews } from '@/mocks/reviews';

export const [ReviewsProvider, useReviews] = createContextHook(() => {
  const queryClient = useQueryClient();

  const reviewsQuery = useQuery({
    queryKey: ['reviews'],
    queryFn: async () => {
      // Simular API call
      await new Promise(resolve => setTimeout(resolve, 500));
      return mockReviews;
    },
  });

  const addReviewMutation = useMutation({
    mutationFn: async (newReview: Omit<Review, 'id' | 'createdAt' | 'helpful'>) => {
      await new Promise(resolve => setTimeout(resolve, 500));
      const review: Review = {
        ...newReview,
        id: Date.now().toString(),
        createdAt: new Date().toISOString(),
        helpful: 0,
      };
      return review;
    },
    onSuccess: (newReview) => {
      queryClient.setQueryData(['reviews'], (old: Review[] = []) => 
        [newReview, ...old]
      );
    },
  });

  const markHelpfulMutation = useMutation({
    mutationFn: async (reviewId: string) => {
      await new Promise(resolve => setTimeout(resolve, 100));
      return reviewId;
    },
    onMutate: async (reviewId) => {
      await queryClient.cancelQueries({ queryKey: ['reviews'] });
      
      queryClient.setQueryData(['reviews'], (old: Review[] = []) =>
        old.map(review =>
          review.id === reviewId
            ? { ...review, helpful: review.helpful + 1 }
            : review
        )
      );
    },
  });

  const getReviewsByProduct = (productId: string) => {
    return reviewsQuery.data?.filter(r => r.productId === productId) || [];
  };

  const getAverageRating = (productId: string) => {
    const reviews = getReviewsByProduct(productId);
    if (reviews.length === 0) return 0;
    return reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length;
  };

  return {
    reviews: reviewsQuery.data || [],
    isLoading: reviewsQuery.isLoading,
    
    addReview: addReviewMutation.mutateAsync,
    markHelpful: markHelpfulMutation.mutate,
    
    getReviewsByProduct,
    getAverageRating,
    
    isAdding: addReviewMutation.isPending,
  };
});

5. Usar en Pantalla

// app/(tabs)/explore/product-reviews.tsx
import { View, StyleSheet, FlatList } from 'react-native';
import { useLocalSearchParams } from 'expo-router';
import { useReviews } from '@/contexts/ReviewsContext';
import { ReviewCard } from '@/components/ReviewCard';
import { useTheme } from '@/contexts/ThemeContext';

export default function ProductReviewsScreen() {
  const { productId } = useLocalSearchParams<{ productId: string }>();
  const { getReviewsByProduct, markHelpful, isLoading } = useReviews();
  const { colors } = useTheme();

  const reviews = getReviewsByProduct(productId);

  return (
    <View style={[styles.container, { backgroundColor: colors.background }]}>
      <FlatList
        data={reviews}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <ReviewCard
            review={item}
            onHelpful={() => markHelpful(item.id)}
          />
        )}
        contentContainerStyle={styles.list}
        ItemSeparatorComponent={() => <View style={styles.separator} />}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  list: { padding: 16 },
  separator: { height: 12 },
});

Integrate External APIs

Structure for External API

// lib/api/weather.ts
const API_KEY = process.env.EXPO_PUBLIC_WEATHER_API_KEY;
const BASE_URL = 'https://api.weatherapi.com/v1';

export interface WeatherData {
  location: string;
  temperature: number;
  condition: string;
  icon: string;
}

export async function getCurrentWeather(city: string): Promise<WeatherData> {
  const response = await fetch(
    `${BASE_URL}/current.json?key=${API_KEY}&q=${city}`
  );

  if (!response.ok) {
    throw new Error('Failed to fetch weather');
  }

  const data = await response.json();
  
  return {
    location: data.location.name,
    temperature: data.current.temp_c,
    condition: data.current.condition.text,
    icon: data.current.condition.icon,
  };
}

Hook for External API

// hooks/useWeather.ts
import { useQuery } from '@tanstack/react-query';
import { getCurrentWeather, WeatherData } from '@/lib/api/weather';

export function useWeather(city: string) {
  return useQuery<WeatherData>({
    queryKey: ['weather', city],
    queryFn: () => getCurrentWeather(city),
    staleTime: 1000 * 60 * 10, // 10 minutos
    enabled: !!city,
  });
}

Add Push Notifications

Instalación

npx expo install expo-notifications expo-device expo-constants

Configuración

// lib/notifications.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
import Constants from 'expo-constants';

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

export async function registerForPushNotifications() {
  if (!Device.isDevice) {
    console.log('Must use physical device for Push Notifications');
    return null;
  }

  const { status: existingStatus } = await Notifications.getPermissionsAsync();
  let finalStatus = existingStatus;

  if (existingStatus !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }

  if (finalStatus !== 'granted') {
    console.log('Failed to get push token for push notification!');
    return null;
  }

  const projectId = Constants.expoConfig?.extra?.eas?.projectId;
  const token = await Notifications.getExpoPushTokenAsync({ projectId });
  
  if (Platform.OS === 'android') {
    Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#FF231F7C',
    });
  }

  return token.data;
}

export async function scheduleLocalNotification(
  title: string,
  body: string,
  trigger?: Notifications.NotificationTriggerInput
) {
  await Notifications.scheduleNotificationAsync({
    content: { title, body },
    trigger: trigger || null,
  });
}

Add Analytics

Con Expo Analytics

// lib/analytics.ts
import * as Analytics from 'expo-firebase-analytics';

export const analytics = {
  logEvent: async (name: string, params?: Record<string, any>) => {
    try {
      await Analytics.logEvent(name, params);
    } catch (error) {
      console.error('Analytics error:', error);
    }
  },

  logScreenView: async (screenName: string) => {
    await analytics.logEvent('screen_view', { screen_name: screenName });
  },

  logPurchase: async (productId: string, price: number, currency: string) => {
    await analytics.logEvent('purchase', {
      item_id: productId,
      value: price,
      currency,
    });
  },

  setUserId: async (userId: string) => {
    try {
      await Analytics.setUserId(userId);
    } catch (error) {
      console.error('Analytics error:', error);
    }
  },
};

New Features Checklist

Before You Start

  • Understand the requirement fully
  • Identify required data types
  • Plan file structure
  • Review existing reusable components

During Development

  • Types defined in types/index.ts
  • Mock data created for development
  • Components with typed props
  • State managed with React Query / Context
  • Navigation configured correctly
  • Styles using constants from colors.ts
  • Dark mode supported
  • Translations added (if using i18n)

Testing

  • Works on iOS
  • Works on Android
  • Works on Web
  • Loading states handled
  • Error states handled
  • Haptic feedback where appropriate

UX/UI

  • Loading states with skeletons
  • Informative empty states
  • Error states with retry
  • Subtle animations
  • Responsive across screen sizes

Code

  • No TypeScript errors
  • testIDs added for testing
  • Console.logs for debugging
  • Comments where needed