Guía 4: Consumo de API con Fetch

React Native con Expo - App móvil de inventario

"La app dejará de trabajar solo en memoria y comenzará a comunicarse con endpoints reales."

Introducción Objetivo Endpoints Servicio API Contexto App.js HomeScreen Categorías Productos Detalle Actividad

1) Introducción

En la guía anterior construimos un CRUD local usando estado y Context API. Eso significa que la aplicación podía crear, editar y eliminar datos, pero esos datos solo existían mientras la app estaba abierta.

En esta guía conectaremos la app con una API usando fetch. De esta manera, las categorías y productos podrán venir desde endpoints externos.

fetch

Es una función de JavaScript que permite hacer peticiones HTTP desde la aplicación.

API

async / await

Nos permite esperar respuestas de la API de una forma más clara y ordenada.

Asincronía

Loading y error

Mostraremos estados de carga y mensajes cuando algo no se pueda obtener.

UX
En esta guía asumiremos que los endpoints ya existen. Si estás usando una API local, recuerda que la app móvil no siempre puede usar localhost directamente.

2) Objetivo de la guía

Al finalizar esta guía, la app podrá consultar, crear, actualizar y eliminar datos comunicándose con una API.

En esta guía aprenderás a:

  • Crear un archivo de servicio para centralizar las peticiones HTTP.
  • Usar fetch para consumir endpoints.
  • Trabajar con GET, POST, PUT y DELETE.
  • Actualizar el contexto para cargar datos desde la API.
  • Manejar estados de carga con ActivityIndicator.
  • Manejar errores con mensajes claros para el usuario.
  • Actualizar el CRUD de categorías y productos para usar funciones asíncronas.
  • Agregar actualización manual usando RefreshControl.

Resultado esperado

La app ahora se comunicará con endpoints reales y mostrará estados de carga, errores y datos dinámicos desde la API.

Parte de la app Resultado
Inicio Mostrará estadísticas usando datos obtenidos desde la API.
Categorías Permitirá crear, editar, eliminar y recargar categorías desde la API.
Productos Permitirá crear, editar, eliminar y recargar productos desde la API.
Estados Mostrará indicadores de carga, manejo de errores y actualización manual.

Estados que verás mientras carga la app

La app pasará por varios estados mientras obtiene datos de la API:

Cargando datos...

Cuando los datos se cargan exitosamente, verás las estadísticas y listas:

Sistema de inventario

Inventario App

5 Categorías
12 Productos

Datos obtenidos desde la API ✓

Si hay un error en la conexión, verás un mensaje claro:

⚠️ Error de conexión

No se pudieron cargar los datos. Verifica tu conexión a internet.

3) Endpoints que usaremos

Para esta guía asumiremos que la API ya tiene endpoints para categorías y productos.

Categorías

GET /categorias

POST /categorias

PUT /categorias/:id

DELETE /categorias/:id

Productos

GET /productos

POST /productos

PUT /productos/:id

DELETE /productos/:id

Forma esperada de una categoría

Ejemplo de categoría
{
  "id": 1,
  "nombre": "Tecnología",
  "descripcion": "Equipos electrónicos y computadoras"
}

Forma esperada de un producto

Ejemplo de producto
{
  "id": 1,
  "nombre": "Laptop Dell",
  "precio": 850,
  "stock": 8,
  "categoriaId": 1
}
Importante: si la API usa otros nombres, por ejemplo category_id en lugar de categoriaId, deberás ajustar el código para que coincida con tu backend.

Sobre localhost

Si tu API corre en tu computadora, probablemente uses una URL parecida a:

URL local común
http://localhost:3000

Sin embargo, desde un teléfono físico, localhost se refiere al teléfono, no a tu computadora. En ese caso deberás usar la IP de tu computadora en la red.

Ejemplo usando IP local
http://192.168.1.25:3000
En los ejemplos usaremos http://192.168.1.25:3000 como URL base. Debes cambiarla por la URL real de tu API.

4) Crear un servicio para la API

Para no escribir fetch directamente en todas las pantallas, crearemos un archivo central llamado api.js.

Dentro de src, crea una carpeta llamada services.

Terminal
mkdir src/services

Ahora crea el archivo:

src/services/api.js

Paso 1: Crear la URL base

Primero escribiremos la dirección principal de la API.

src/services/api.js
const API_URL = 'http://192.168.1.25:3000';

Esta URL se usará como base para construir las rutas de categorías y productos.

Paso 2: Crear función request paso a paso

Paso 2.1: Estructura básica de request

En lugar de repetir fetch en cada pantalla, crearemos una función central.

Función básica
async function request(endpoint, options = {}) {
  const response = await fetch(`${API_URL}${endpoint}`, options);

  return response.json();
}

Esta función:

Paso 3: Agregar headers JSON paso a paso

Paso 3.1: Estructura con headers

Cuando enviamos datos (POST, PUT), debemos indicar que es JSON:

Función con headers
async function request(endpoint, options = {}) {
  const response = await fetch(`${API_URL}${endpoint}`, {
    headers: {
      'Content-Type': 'application/json',
      ...options.headers,
    },
    ...options,
  });

  return response.json();
}

El spread operator ...options.headers permite agregar headers adicionales si fueran necesarios.

Paso 4: Validar errores HTTP paso a paso

Paso 4.1: Detectar respuestas fallidas

Si la API devuelve error (4xx, 5xx), response.ok será falso:

Validación de error
if (!response.ok) {
  throw new Error('Ocurrió un error al comunicarse con la API.');
}

Esta validación se coloca después de fetch y antes de leer el JSON.

Paso 5: Manejar respuestas vacías

Algunas APIs responden con código 204 No Content cuando eliminan un registro. No hay JSON que leer en ese caso:

Respuesta vacía (204)
if (response.status === 204) {
  return null;
}

Paso 6: Crear funciones para categorías

Ahora crearemos funciones específicas para cada operación de categorías.

Funciones de categorías
function getCategorias() {
  return request('/categorias');
}

function createCategoria(categoria) {
  return request('/categorias', {
    method: 'POST',
    body: JSON.stringify(categoria),
  });
}

function updateCategoria(id, categoria) {
  return request(`/categorias/${id}`, {
    method: 'PUT',
    body: JSON.stringify(categoria),
  });
}

function deleteCategoria(id) {
  return request(`/categorias/${id}`, {
    method: 'DELETE',
  });
}

Paso 7: Crear funciones para productos

Haremos lo mismo con productos.

Funciones de productos
function getProductos() {
  return request('/productos');
}

function createProducto(producto) {
  return request('/productos', {
    method: 'POST',
    body: JSON.stringify(producto),
  });
}

function updateProducto(id, producto) {
  return request(`/productos/${id}`, {
    method: 'PUT',
    body: JSON.stringify(producto),
  });
}

function deleteProducto(id) {
  return request(`/productos/${id}`, {
    method: 'DELETE',
  });
}

Paso 8: Exportar el objeto api

Finalmente, agruparemos todas las funciones dentro de un objeto llamado api.

Exportar api
export const api = {
  getCategorias,
  createCategoria,
  updateCategoria,
  deleteCategoria,
  getProductos,
  createProducto,
  updateProducto,
  deleteProducto,
};

Código final de api.js

5) Actualizar InventoryContext para usar la API

En la guía anterior, el contexto tenía datos escritos directamente en arreglos. Ahora esos datos vendrán desde la API.

Abre el archivo:

src/context/InventoryContext.js

Paso 1: Agregar useEffect

Como cargaremos datos cuando la app inicie, necesitamos useEffect.

Importación actualizada
import { createContext, useContext, useEffect, useState } from 'react';

Paso 2: Importar el servicio api

También importaremos el archivo que creamos en la sección anterior.

Importar api
import { api } from '../services/api';

Paso 3: Cambiar los estados iniciales

Ya no iniciaremos con datos escritos a mano. Ahora las listas comienzan vacías.

Estados principales
const [categorias, setCategorias] = useState([]);
const [productos, setProductos] = useState([]);

Además, agregaremos estados para carga y error.

Estados de carga y error
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

Paso 4: Crear función para cargar categorías

Esta función consultará /categorias y actualizará el estado.

cargarCategorias
async function cargarCategorias() {
  const datos = await api.getCategorias();
  setCategorias(datos);
}

Paso 5: Crear función para cargar productos

Ahora hacemos lo mismo con productos.

cargarProductos
async function cargarProductos() {
  const datos = await api.getProductos();
  setProductos(datos);
}

Paso 6: Crear función para cargar todo paso a paso

En lugar de cargar cada lista por separado, crearemos una función general que maneje carga y errores.

Paso 6.1: Estructura básica con try-catch

Estructura inicial
async function cargarDatos() {
  try {
    // Aquí va el código que intenta cargar
  } catch (error) {
    // Aquí va el manejo de errores
  } finally {
    // Aquí va lo que siempre se ejecuta
  }
}

Paso 6.2: Indicar que está cargando

Al inicio del try, establecemos que está cargando y limpiamos errores previos:

Inicio de carga
  try {
    setLoading(true);
    setError(null);

Paso 6.3: Obtener datos de la API

Obtener datos
    const categoriasApi = await api.getCategorias();
    const productosApi = await api.getProductos();

Paso 6.4: Actualizar el estado

Guardar datos
    setCategorias(categoriasApi);
    setProductos(productosApi);

Paso 6.5: Manejar errores

Capturar errores
  } catch (error) {
    setError('No se pudieron cargar los datos.');
  } finally {
    setLoading(false);
  }

finally siempre se ejecuta, indicando que la carga terminó (con éxito o error).

Paso 7: Ejecutar cargarDatos al iniciar

Usaremos useEffect para cargar datos cuando la aplicación inicie.

useEffect inicial
useEffect(() => {
  cargarDatos();
}, []);

El arreglo vacío [] indica que esto se ejecutará una sola vez cuando el componente cargue.

Vista previa: Estados de carga

Durante la carga de datos desde la API, verás algo así:

Cargando datos...

Conectando a la API...

Una vez que se cargan los datos exitosamente, la pantalla mostrará:

Datos cargados

Laptop Dell XPS

$850.00 - Stock: 8

Tecnología
Mouse inalámbrico

$18.50 - Stock: 25

Accesorios

Paso 8: Adaptar agregarCategoria

En la guía anterior, agregar una categoría solo modificaba el estado local. Ahora deberá hacer una petición POST.

agregarCategoria con API
async function agregarCategoria(nuevaCategoria) {
  await api.createCategoria(nuevaCategoria);
  await cargarCategorias();
}

Después de crear la categoría, volvemos a cargar la lista para mostrar los datos actualizados.

Paso 9: Adaptar actualizarCategoria

actualizarCategoria con API
async function actualizarCategoria(categoriaActualizada) {
  await api.updateCategoria(categoriaActualizada.id, categoriaActualizada);
  await cargarCategorias();
}

Paso 10: Adaptar eliminarCategoria

eliminarCategoria con API
async function eliminarCategoria(id) {
  await api.deleteCategoria(id);
  await cargarCategorias();
}

Paso 11: Adaptar productos

Repetiremos la misma idea para productos.

CRUD de productos con API
async function agregarProducto(nuevoProducto) {
  await api.createProducto(nuevoProducto);
  await cargarProductos();
}

async function actualizarProducto(productoActualizado) {
  await api.updateProducto(productoActualizado.id, productoActualizado);
  await cargarProductos();
}

async function eliminarProducto(id) {
  await api.deleteProducto(id);
  await cargarProductos();
}

Paso 12: Compartir loading, error y cargarDatos

Agregaremos estos elementos al value del contexto para que las pantallas puedan usarlos.

Value actualizado
const value = {
  categorias,
  productos,
  loading,
  error,
  cargarDatos,
  cargarCategorias,
  cargarProductos,
  agregarCategoria,
  actualizarCategoria,
  eliminarCategoria,
  agregarProducto,
  actualizarProducto,
  eliminarProducto,
};

Código final de InventoryContext.js

6) Revisar App.js

El archivo App.js no cambia mucho respecto a la guía anterior. Solamente debemos asegurarnos de que la app esté envuelta con InventoryProvider.

El provider es importante porque desde ahí se cargan los datos iniciales de la API.

7) Actualizar HomeScreen con carga, error y actualización

Ahora la pantalla de inicio debe tomar en cuenta que los datos vienen desde internet. Eso significa que puede haber un momento de carga o un error.

Abre el archivo:

src/screens/HomeScreen.js

Paso 1: Importar ActivityIndicator y RefreshControl

ActivityIndicator mostrará una animación de carga. RefreshControl permitirá recargar al deslizar hacia abajo.

Importación actualizada
import {
  ActivityIndicator,
  RefreshControl,
  ScrollView,
  View,
  Text,
  StyleSheet,
  Pressable,
} from 'react-native';

Paso 2: Agregar estado refreshing

Este estado controlará la actualización manual.

Estado refreshing
const [refreshing, setRefreshing] = useState(false);

Recuerda que para usarlo debes importar useState.

Importar useState
import { useState } from 'react';

Paso 3: Obtener más datos del contexto

Antes solo obteníamos categorías y productos. Ahora también necesitamos loading, error y cargarDatos.

Usar contexto
const {
  categorias,
  productos,
  loading,
  error,
  cargarDatos,
} = useInventory();

Paso 4: Crear función para refrescar

Esta función llamará nuevamente a la API.

Función onRefresh
async function onRefresh() {
  setRefreshing(true);
  await cargarDatos();
  setRefreshing(false);
}

Paso 5: Mostrar pantalla de carga

Si la app está cargando y todavía no hay productos ni categorías, mostraremos una pantalla simple.

Pantalla de carga
if (loading && productos.length === 0 && categorias.length === 0) {
  return (
    <View style={styles.centerContent}>
      <ActivityIndicator size="large" color="#1a5276" />
      <Text style={styles.centerText}>Cargando inventario...</Text>
    </View>
  );
}

Paso 6: Agregar RefreshControl al ScrollView

El ScrollView recibirá una propiedad llamada refreshControl.

ScrollView con RefreshControl
<ScrollView
  contentContainerStyle={styles.content}
  refreshControl={
    <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
  }
>

Paso 7: Mostrar error si existe

Dentro del ScrollView, debajo del encabezado, podemos mostrar un mensaje si ocurrió un error.

Mensaje de error
{error && (
  <View style={styles.errorBox}>
    <Text style={styles.errorText}>{error}</Text>
    <Pressable style={styles.retryButton} onPress={cargarDatos}>
      <Text style={styles.retryButtonText}>Reintentar</Text>
    </Pressable>
  </View>
)}

Paso 8: Agregar estilos nuevos

Estilos nuevos
centerContent: {
  flex: 1,
  justifyContent: 'center',
  alignItems: 'center',
  backgroundColor: '#f8fafc',
  padding: 20,
},
centerText: {
  marginTop: 12,
  color: '#64748b',
  fontWeight: '800',
},
errorBox: {
  backgroundColor: '#fee2e2',
  padding: 14,
  borderRadius: 14,
  marginBottom: 14,
},
errorText: {
  color: '#991b1b',
  fontWeight: '800',
  marginBottom: 10,
},
retryButton: {
  backgroundColor: '#991b1b',
  padding: 10,
  borderRadius: 10,
},
retryButtonText: {
  color: '#ffffff',
  textAlign: 'center',
  fontWeight: '900',
},
No es necesario reescribir todo el archivo si ya lo tienes funcionando. Solo debes agregar los imports, el estado refreshing, la función onRefresh, la pantalla de carga y el mensaje de error.

8) Actualizar el CRUD de categorías para usar API

En la Guía 3, las funciones de categorías eran locales. Ahora esas funciones son asíncronas porque se comunican con la API.

Abre el archivo:

src/screens/CategoriesScreen.js

Paso 1: Crear estado guardando

Este estado evitará que el usuario presione muchas veces el botón mientras se guarda.

Estado guardando
const [guardando, setGuardando] = useState(false);

Paso 2: Modificar guardarCategoria para que sea async

Antes la función era normal. Ahora deberá usar async.

Antes
function guardarCategoria() {
  // código
}
Después
async function guardarCategoria() {
  // código
}

Paso 3: Quitar el ID al crear una categoría nueva

Cuando trabajamos localmente usábamos Date.now() para crear un ID. Pero cuando usamos API, normalmente el backend crea el ID.

Objeto base de categoría
const categoria = {
  nombre: nombre.trim(),
  descripcion: descripcion.trim(),
};

Si estamos editando, entonces sí agregamos el ID.

Agregar ID solo al editar
if (categoriaEditandoId) {
  categoria.id = categoriaEditandoId;
}

Paso 4: Usar try/catch para manejar errores

Paso 4.1: Indicar que está guardando

Al inicio del try, establecemos que está guardando (para deshabilitar botones):

Inicio de guardado
try {
  setGuardando(true);

Paso 4.2: Llamar a la función del contexto

Las funciones del contexto ya se encargan de hacer la petición a la API:

Guardar en API
  if (categoriaEditandoId) {
    await actualizarCategoria(categoria);
  } else {
    await agregarCategoria(categoria);
  }

Paso 4.3: Manejar errores y finalizar

Capturar errores y limpiar
  limpiarFormulario();
  Alert.alert('Éxito', 'Categoría guardada correctamente.');
} catch (error) {
  Alert.alert('Error', 'No se pudo guardar. Intenta de nuevo.');
} finally {
  setGuardando(false);
}

El finally siempre se ejecuta para indicar que terminó el guardado, permitiendo que el usuario intente de nuevo si es necesario.

Vista previa: Estados de guardado

Mientras se está guardando la categoría, el botón quedará deshabilitado:

Nueva categoría

Después de guardar, verás una notificación de éxito:

✓ Categoría guardada correctamente.

Nueva categoría

Si hay un error en la conexión, verás una notificación de error:

✗ No se pudo guardar. Intenta de nuevo.

Nueva categoría

guardarCategoria completa
async function guardarCategoria() {
  try {
    setGuardando(true);

    if (categoriaEditandoId) {
      await actualizarCategoria(categoria);
      Alert.alert('Categoría actualizada', 'Los datos fueron actualizados.');
    } else {
      await agregarCategoria(categoria);
      Alert.alert('Categoría creada', 'La categoría fue registrada.');
    }

    limpiarFormulario();
  } catch (error) {
    Alert.alert('Error', 'No se pudo guardar la categoría.');
  } finally {
    setGuardando(false);
  }
}

Paso 5: Desactivar el botón mientras guarda

Modificaremos el botón para que use disabled.

Botón guardar actualizado
<Pressable
  style={styles.primaryButton}
  onPress={guardarCategoria}
  disabled={guardando}
>
  <Text style={styles.primaryButtonText}>
    {guardando
      ? 'Guardando...'
      : categoriaEditandoId
        ? 'Actualizar categoría'
        : 'Guardar categoría'}
  </Text>
</Pressable>

Paso 6: Actualizar eliminación

La eliminación también debe esperar la respuesta de la API.

Eliminar con async
onPress: async () => {
  try {
    await eliminarCategoria(id);
    Alert.alert('Categoría eliminada', 'La categoría fue eliminada.');
  } catch (error) {
    Alert.alert('Error', 'No se pudo eliminar la categoría.');
  }
},

Paso 7: Agregar RefreshControl

También podemos permitir que la lista de categorías se recargue manualmente.

Importar RefreshControl
import {
  Alert,
  RefreshControl,
  ScrollView,
  View,
  Text,
  TextInput,
  Pressable,
  StyleSheet,
} from 'react-native';

Del contexto obtenemos cargarCategorias.

Obtener cargarCategorias
const {
  categorias,
  productos,
  cargarCategorias,
  agregarCategoria,
  actualizarCategoria,
  eliminarCategoria,
} = useInventory();

Creamos la función para refrescar:

Función refrescar categorías
const [refreshing, setRefreshing] = useState(false);

async function onRefresh() {
  setRefreshing(true);
  await cargarCategorias();
  setRefreshing(false);
}

Y la agregamos al ScrollView.

ScrollView con RefreshControl
<ScrollView
  style={styles.container}
  contentContainerStyle={styles.content}
  refreshControl={
    <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
  }
>
Resumen:

En categorías, los cambios principales son: usar async/await, eliminar Date.now() al crear, manejar errores y recargar datos desde la API.

9) Actualizar el CRUD de productos para usar API

Ahora haremos cambios similares en productos.

Abre el archivo:

src/screens/ProductsScreen.js

Paso 1: Crear estado guardando

Estado guardando
const [guardando, setGuardando] = useState(false);

Paso 2: Modificar guardarProducto para que sea async

Antes
function guardarProducto() {
  // código
}
Después
async function guardarProducto() {
  // código
}

Paso 3: Crear el objeto producto sin ID al registrar

Igual que con categorías, el backend normalmente generará el ID.

Objeto producto
const producto = {
  nombre: nombre.trim(),
  precio: precioNumero,
  stock: stockNumero,
  categoriaId: categoriaSeleccionadaId,
};

Si estamos editando, agregamos el ID.

Agregar ID al editar
if (productoEditandoId) {
  producto.id = productoEditandoId;
}

Paso 4: Guardar producto con try/catch paso a paso

Paso 4.1: Indicar que está guardando y diferenciar operación

Inicio de guardado
try {
  setGuardando(true);

  if (productoEditandoId) {
    await actualizarProducto(producto);

Paso 4.2: Mostrar mensajes de éxito según la operación

El usuario recibe feedback diferente según si creó o actualizó:

Mensajes de éxito
    Alert.alert('Producto actualizado', 'Los datos fueron actualizados.');
  } else {
    await agregarProducto(producto);
    Alert.alert('Producto creado', 'El producto fue registrado.');
  }

Paso 4.3: Limpiar y manejar errores

Finalizar operación
  limpiarFormulario();
} catch (error) {
  Alert.alert('Error', 'No se pudo guardar el producto. Intenta de nuevo.');
} finally {
  setGuardando(false);
}
limpiarFormulario(); } catch (error) { Alert.alert('Error', 'No se pudo guardar el producto.'); } finally { setGuardando(false); }

Paso 5: Actualizar el botón de guardar

Botón guardar producto
<Pressable
  style={styles.primaryButton}
  onPress={guardarProducto}
  disabled={guardando}
>
  <Text style={styles.primaryButtonText}>
    {guardando
      ? 'Guardando...'
      : productoEditandoId
        ? 'Actualizar producto'
        : 'Guardar producto'}
  </Text>
</Pressable>

Paso 6: Actualizar eliminación de productos

Eliminar producto con API
onPress: async () => {
  try {
    await eliminarProducto(id);
    Alert.alert('Producto eliminado', 'El producto fue eliminado.');
  } catch (error) {
    Alert.alert('Error', 'No se pudo eliminar el producto.');
  }
},

Paso 7: Cambiar navegación al detalle

Antes enviábamos todo el producto a la pantalla de detalle. Ahora enviaremos solamente el ID.

Antes
navigation.navigate('DetalleProducto', {
  producto: producto,
  categoria: obtenerNombreCategoria(producto.categoriaId),
})
Después
navigation.navigate('DetalleProducto', {
  productoId: producto.id,
})

Esto es mejor porque el detalle podrá buscar el producto actualizado desde el contexto.

Paso 8: Agregar RefreshControl a productos

Del contexto obtenemos cargarProductos.

Obtener cargarProductos
const {
  categorias,
  productos,
  cargarProductos,
  agregarProducto,
  actualizarProducto,
  eliminarProducto,
} = useInventory();

Creamos la función:

Refrescar productos
const [refreshing, setRefreshing] = useState(false);

async function onRefresh() {
  setRefreshing(true);
  await cargarProductos();
  setRefreshing(false);
}

Y agregamos RefreshControl al ScrollView.

ScrollView actualizado
<ScrollView
  style={styles.container}
  contentContainerStyle={styles.content}
  refreshControl={
    <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
  }
>
Con estos cambios, productos ya no depende solo de memoria local. Ahora crea, actualiza, elimina y recarga datos desde la API.

10) Actualizar ProductDetailScreen

Para que el detalle muestre información actualizada, usaremos el productoId recibido por navegación y buscaremos el producto dentro del contexto.

Abre el archivo:

src/screens/ProductDetailScreen.js

Paso 1: Importar useInventory

Importar contexto
import { useInventory } from '../context/InventoryContext';

Paso 2: Obtener el productoId

Ya no recibimos todo el objeto producto. Ahora recibimos solo el ID.

Recibir productoId
const { productoId } = route.params;

Paso 3: Obtener productos y categorías del contexto

Usar contexto
const { productos, categorias } = useInventory();

Paso 4: Buscar el producto

Buscar producto
const producto = productos.find(producto => producto.id === productoId);

Paso 5: Buscar la categoría

Buscar categoría
const categoria = categorias.find(
  categoria => categoria.id === producto?.categoriaId
);

Usamos producto?.categoriaId para evitar errores si el producto no existe.

Paso 6: Validar si el producto no existe

Esto puede ocurrir si el producto fue eliminado.

Validación de producto
if (!producto) {
  return (
    <View style={styles.container}>
      <View style={styles.card}>
        <Text style={styles.title}>Producto no encontrado</Text>

        <Pressable style={styles.button} onPress={() => navigation.goBack()}>
          <Text style={styles.buttonText}>Volver</Text>
        </Pressable>
      </View>
    </View>
  );
}

Paso 7: Código final de ProductDetailScreen.js

Resumen: Estados de la app con API

Después de implementar todo, la app mostrará diferentes estados:

Estado 1: Guardando una categoría/producto

Nueva categoría

El botón está deshabilitado mientras se guarda

Estado 2: Éxito al guardar

✓ Producto creado

El producto fue registrado en la API.

Estado 3: Error al guardar

⚠️ Error

No se pudo guardar el producto. Intenta de nuevo.

11) Mejoras finales de experiencia

Para que la aplicación se sienta más completa, podemos agregar algunos detalles adicionales.

Mensaje cuando no hay categorías

En CategoriesScreen.js, antes de recorrer categorías, puedes agregar:

Mensaje sin categorías
{categorias.length === 0 && (
  <View style={styles.emptyBox}>
    <Text style={styles.emptyText}>No hay categorías registradas.</Text>
  </View>
)}

Mensaje cuando no hay productos

En ProductsScreen.js, antes de recorrer productos, puedes agregar:

Mensaje sin productos
{productos.length === 0 && (
  <View style={styles.emptyBox}>
    <Text style={styles.emptyText}>No hay productos registrados.</Text>
  </View>
)}

Estilos para mensajes vacíos

Estilos emptyBox
emptyBox: {
  backgroundColor: '#ffffff',
  padding: 18,
  borderRadius: 18,
  borderWidth: 1,
  borderColor: '#e2e8f0',
},
emptyText: {
  color: '#64748b',
  textAlign: 'center',
  fontWeight: '800',
},

Mostrar error específico

En algunos catch, puedes revisar el error en consola para depurar:

Debug de error
catch (error) {
  console.log(error);
  Alert.alert('Error', 'No se pudo completar la operación.');
}
Recomendación: en una app real, evita mostrar errores técnicos al usuario final. Usa mensajes claros como "No se pudo guardar" o "Revisa tu conexión".

12) Actividad práctica

Ahora es tu turno. Realiza las siguientes pruebas y mejoras.

Ejercicio 1

Cambia la constante API_URL por la URL real de tu API.

Ejercicio 2

Ejecuta la app y verifica que las categorías y productos se carguen desde la API.

Ejercicio 3

Crea una nueva categoría y verifica en el backend que realmente se haya registrado.

Ejercicio 4

Crea un nuevo producto y asígnalo a una categoría existente.

Ejercicio 5

Edita un producto y verifica que los cambios se mantengan después de cerrar y abrir la app.

Ejercicio 6

Apaga temporalmente la API y revisa si la app muestra un mensaje de error.

Checklist de revisión

Con esta guía final, la aplicación ya cuenta con layout moderno, navegación, CRUD de categorías, CRUD de productos y consumo de API con fetch.