React Native con Expo - App móvil de inventario
"La app dejará de trabajar solo en memoria y comenzará a comunicarse con endpoints reales."
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.
Es una función de JavaScript que permite hacer peticiones HTTP desde la aplicación.
APINos permite esperar respuestas de la API de una forma más clara y ordenada.
AsincroníaMostraremos estados de carga y mensajes cuando algo no se pueda obtener.
UXlocalhost directamente.
Al finalizar esta guía, la app podrá consultar, crear, actualizar y eliminar datos comunicándose con una API.
fetch para consumir endpoints.GET, POST, PUT y DELETE.ActivityIndicator.RefreshControl.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. |
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:
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.
Para esta guía asumiremos que la API ya tiene endpoints para categorías y productos.
GET /categorias
POST /categorias
PUT /categorias/:id
DELETE /categorias/:id
GET /productos
POST /productos
PUT /productos/:id
DELETE /productos/:id
{
"id": 1,
"nombre": "Tecnología",
"descripcion": "Equipos electrónicos y computadoras"
}
{
"id": 1,
"nombre": "Laptop Dell",
"precio": 850,
"stock": 8,
"categoriaId": 1
}
category_id
en lugar de categoriaId, deberás ajustar el código para que coincida con tu backend.
Si tu API corre en tu computadora, probablemente uses una URL parecida a:
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.
http://192.168.1.25:3000
http://192.168.1.25:3000 como URL base.
Debes cambiarla por la URL real de tu 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.
mkdir src/services
Ahora crea el archivo:
Primero escribiremos la dirección principal de la API.
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.1: Estructura básica de request
En lugar de repetir fetch en cada pantalla, crearemos una función central.
async function request(endpoint, options = {}) {
const response = await fetch(`${API_URL}${endpoint}`, options);
return response.json();
}
Esta función:
endpoint como /productosPaso 3.1: Estructura con headers
Cuando enviamos datos (POST, PUT), debemos indicar que es JSON:
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.1: Detectar respuestas fallidas
Si la API devuelve error (4xx, 5xx), response.ok será falso:
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.
Algunas APIs responden con código 204 No Content cuando eliminan un registro.
No hay JSON que leer en ese caso:
if (response.status === 204) {
return null;
}
Ahora crearemos funciones específicas para cada operación 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',
});
}
Haremos lo mismo con 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',
});
}
Finalmente, agruparemos todas las funciones dentro de un objeto llamado api.
export const api = {
getCategorias,
createCategoria,
updateCategoria,
deleteCategoria,
getProductos,
createProducto,
updateProducto,
deleteProducto,
};
const API_URL = 'http://192.168.1.25:3000';
async function request(endpoint, options = {}) {
const response = await fetch(`${API_URL}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
throw new Error('Ocurrió un error al comunicarse con la API.');
}
if (response.status === 204) {
return null;
}
return response.json();
}
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',
});
}
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',
});
}
export const api = {
getCategorias,
createCategoria,
updateCategoria,
deleteCategoria,
getProductos,
createProducto,
updateProducto,
deleteProducto,
};
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:
Como cargaremos datos cuando la app inicie, necesitamos useEffect.
import { createContext, useContext, useEffect, useState } from 'react';
También importaremos el archivo que creamos en la sección anterior.
import { api } from '../services/api';
Ya no iniciaremos con datos escritos a mano. Ahora las listas comienzan vacías.
const [categorias, setCategorias] = useState([]);
const [productos, setProductos] = useState([]);
Además, agregaremos estados para carga y error.
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
Esta función consultará /categorias y actualizará el estado.
async function cargarCategorias() {
const datos = await api.getCategorias();
setCategorias(datos);
}
Ahora hacemos lo mismo con productos.
async function cargarProductos() {
const datos = await api.getProductos();
setProductos(datos);
}
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
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:
try {
setLoading(true);
setError(null);
Paso 6.3: Obtener datos de la API
const categoriasApi = await api.getCategorias();
const productosApi = await api.getProductos();
Paso 6.4: Actualizar el estado
setCategorias(categoriasApi);
setProductos(productosApi);
Paso 6.5: Manejar 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).
Usaremos useEffect para cargar datos cuando la aplicación inicie.
useEffect(() => {
cargarDatos();
}, []);
El arreglo vacío [] indica que esto se ejecutará una sola vez cuando el componente cargue.
Durante la carga de datos desde la API, verás algo así:
Conectando a la API...
Una vez que se cargan los datos exitosamente, la pantalla mostrará:
$850.00 - Stock: 8
Tecnología$18.50 - Stock: 25
Accesorios
En la guía anterior, agregar una categoría solo modificaba el estado local.
Ahora deberá hacer una petición POST.
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.
async function actualizarCategoria(categoriaActualizada) {
await api.updateCategoria(categoriaActualizada.id, categoriaActualizada);
await cargarCategorias();
}
async function eliminarCategoria(id) {
await api.deleteCategoria(id);
await cargarCategorias();
}
Repetiremos la misma idea para productos.
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();
}
Agregaremos estos elementos al value del contexto para que las pantallas puedan usarlos.
const value = {
categorias,
productos,
loading,
error,
cargarDatos,
cargarCategorias,
cargarProductos,
agregarCategoria,
actualizarCategoria,
eliminarCategoria,
agregarProducto,
actualizarProducto,
eliminarProducto,
};
import { createContext, useContext, useEffect, useState } from 'react';
import { api } from '../services/api';
const InventoryContext = createContext();
export function InventoryProvider({ children }) {
const [categorias, setCategorias] = useState([]);
const [productos, setProductos] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
async function cargarCategorias() {
const datos = await api.getCategorias();
setCategorias(datos);
}
async function cargarProductos() {
const datos = await api.getProductos();
setProductos(datos);
}
async function cargarDatos() {
try {
setLoading(true);
setError(null);
const categoriasApi = await api.getCategorias();
const productosApi = await api.getProductos();
setCategorias(categoriasApi);
setProductos(productosApi);
} catch (error) {
setError('No se pudieron cargar los datos.');
} finally {
setLoading(false);
}
}
useEffect(() => {
cargarDatos();
}, []);
async function agregarCategoria(nuevaCategoria) {
await api.createCategoria(nuevaCategoria);
await cargarCategorias();
}
async function actualizarCategoria(categoriaActualizada) {
await api.updateCategoria(categoriaActualizada.id, categoriaActualizada);
await cargarCategorias();
}
async function eliminarCategoria(id) {
await api.deleteCategoria(id);
await cargarCategorias();
}
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();
}
const value = {
categorias,
productos,
loading,
error,
cargarDatos,
cargarCategorias,
cargarProductos,
agregarCategoria,
actualizarCategoria,
eliminarCategoria,
agregarProducto,
actualizarProducto,
eliminarProducto,
};
return (
<InventoryContext.Provider value={value}>
{children}
</InventoryContext.Provider>
);
}
export function useInventory() {
return useContext(InventoryContext);
}
El archivo App.js no cambia mucho respecto a la guía anterior.
Solamente debemos asegurarnos de que la app esté envuelta con InventoryProvider.
import 'react-native-gesture-handler';
import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import RootDrawer from './src/navigation/RootDrawer';
import { InventoryProvider } from './src/context/InventoryContext';
export default function App() {
return (
<SafeAreaProvider>
<InventoryProvider>
<NavigationContainer>
<RootDrawer />
</NavigationContainer>
</InventoryProvider>
<StatusBar style="light" />
</SafeAreaProvider>
);
}
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:
ActivityIndicator mostrará una animación de carga.
RefreshControl permitirá recargar al deslizar hacia abajo.
import {
ActivityIndicator,
RefreshControl,
ScrollView,
View,
Text,
StyleSheet,
Pressable,
} from 'react-native';
Este estado controlará la actualización manual.
const [refreshing, setRefreshing] = useState(false);
Recuerda que para usarlo debes importar useState.
import { useState } from 'react';
Antes solo obteníamos categorías y productos.
Ahora también necesitamos loading, error y cargarDatos.
const {
categorias,
productos,
loading,
error,
cargarDatos,
} = useInventory();
Esta función llamará nuevamente a la API.
async function onRefresh() {
setRefreshing(true);
await cargarDatos();
setRefreshing(false);
}
Si la app está cargando y todavía no hay productos ni categorías, mostraremos una pantalla simple.
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>
);
}
El ScrollView recibirá una propiedad llamada refreshControl.
<ScrollView
contentContainerStyle={styles.content}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
Dentro del ScrollView, debajo del encabezado, podemos mostrar un mensaje si ocurrió un
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>
)}
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',
},
refreshing, la función
onRefresh, la pantalla de carga y el mensaje de error.
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:
Este estado evitará que el usuario presione muchas veces el botón mientras se guarda.
const [guardando, setGuardando] = useState(false);
Antes la función era normal. Ahora deberá usar async.
function guardarCategoria() {
// código
}
async function guardarCategoria() {
// código
}
Cuando trabajamos localmente usábamos Date.now() para crear un ID.
Pero cuando usamos API, normalmente el backend crea el ID.
const categoria = {
nombre: nombre.trim(),
descripcion: descripcion.trim(),
};
Si estamos editando, entonces sí agregamos el ID.
if (categoriaEditandoId) {
categoria.id = categoriaEditandoId;
}
Paso 4.1: Indicar que está guardando
Al inicio del try, establecemos que está guardando (para deshabilitar botones):
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:
if (categoriaEditandoId) {
await actualizarCategoria(categoria);
} else {
await agregarCategoria(categoria);
}
Paso 4.3: Manejar errores y finalizar
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.
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:
Nueva categoría
Si hay un error en la conexión, verás una notificación de error:
Nueva categoría
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);
}
}
Modificaremos el botón para que use disabled.
<Pressable
style={styles.primaryButton}
onPress={guardarCategoria}
disabled={guardando}
>
<Text style={styles.primaryButtonText}>
{guardando
? 'Guardando...'
: categoriaEditandoId
? 'Actualizar categoría'
: 'Guardar categoría'}
</Text>
</Pressable>
La eliminación también debe esperar la respuesta de la API.
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.');
}
},
También podemos permitir que la lista de categorías se recargue manualmente.
import {
Alert,
RefreshControl,
ScrollView,
View,
Text,
TextInput,
Pressable,
StyleSheet,
} from 'react-native';
Del contexto obtenemos cargarCategorias.
const {
categorias,
productos,
cargarCategorias,
agregarCategoria,
actualizarCategoria,
eliminarCategoria,
} = useInventory();
Creamos la función para refrescar:
const [refreshing, setRefreshing] = useState(false);
async function onRefresh() {
setRefreshing(true);
await cargarCategorias();
setRefreshing(false);
}
Y la agregamos al ScrollView.
<ScrollView
style={styles.container}
contentContainerStyle={styles.content}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
En categorías, los cambios principales son: usar async/await,
eliminar Date.now() al crear, manejar errores y recargar datos desde la API.
Ahora haremos cambios similares en productos.
Abre el archivo:
const [guardando, setGuardando] = useState(false);
function guardarProducto() {
// código
}
async function guardarProducto() {
// código
}
Igual que con categorías, el backend normalmente generará el ID.
const producto = {
nombre: nombre.trim(),
precio: precioNumero,
stock: stockNumero,
categoriaId: categoriaSeleccionadaId,
};
Si estamos editando, agregamos el ID.
if (productoEditandoId) {
producto.id = productoEditandoId;
}
Paso 4.1: Indicar que está guardando y diferenciar operación
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ó:
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
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);
}
<Pressable
style={styles.primaryButton}
onPress={guardarProducto}
disabled={guardando}
>
<Text style={styles.primaryButtonText}>
{guardando
? 'Guardando...'
: productoEditandoId
? 'Actualizar producto'
: 'Guardar producto'}
</Text>
</Pressable>
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.');
}
},
Antes enviábamos todo el producto a la pantalla de detalle. Ahora enviaremos solamente el ID.
navigation.navigate('DetalleProducto', {
producto: producto,
categoria: obtenerNombreCategoria(producto.categoriaId),
})
navigation.navigate('DetalleProducto', {
productoId: producto.id,
})
Esto es mejor porque el detalle podrá buscar el producto actualizado desde el contexto.
Del contexto obtenemos cargarProductos.
const {
categorias,
productos,
cargarProductos,
agregarProducto,
actualizarProducto,
eliminarProducto,
} = useInventory();
Creamos la función:
const [refreshing, setRefreshing] = useState(false);
async function onRefresh() {
setRefreshing(true);
await cargarProductos();
setRefreshing(false);
}
Y agregamos RefreshControl al ScrollView.
<ScrollView
style={styles.container}
contentContainerStyle={styles.content}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
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:
import { useInventory } from '../context/InventoryContext';
Ya no recibimos todo el objeto producto. Ahora recibimos solo el ID.
const { productoId } = route.params;
const { productos, categorias } = useInventory();
const producto = productos.find(producto => producto.id === productoId);
const categoria = categorias.find(
categoria => categoria.id === producto?.categoriaId
);
Usamos producto?.categoriaId para evitar errores si el producto no existe.
Esto puede ocurrir si el producto fue eliminado.
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>
);
}
import { View, Text, StyleSheet, Pressable } from 'react-native';
import { useInventory } from '../context/InventoryContext';
export default function ProductDetailScreen({ route, navigation }) {
const { productoId } = route.params;
const { productos, categorias } = useInventory();
const producto = productos.find(producto => producto.id === productoId);
const categoria = categorias.find(
categoria => categoria.id === producto?.categoriaId
);
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>
);
}
return (
<View style={styles.container}>
<View style={styles.card}>
<Text style={styles.kicker}>Detalle del producto</Text>
<Text style={styles.title}>{producto.nombre}</Text>
<View style={styles.infoBox}>
<Text style={styles.label}>Categoría</Text>
<Text style={styles.value}>
{categoria ? categoria.nombre : 'Sin categoría'}
</Text>
</View>
<View style={styles.infoBox}>
<Text style={styles.label}>Precio</Text>
<Text style={styles.value}>${producto.precio.toFixed(2)}</Text>
</View>
<View style={styles.infoBox}>
<Text style={styles.label}>Stock disponible</Text>
<Text style={styles.value}>{producto.stock} unidades</Text>
</View>
<Pressable style={styles.button} onPress={() => navigation.goBack()}>
<Text style={styles.buttonText}>Volver a productos</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
padding: 20,
},
card: {
backgroundColor: '#ffffff',
borderRadius: 22,
padding: 22,
borderWidth: 1,
borderColor: '#e2e8f0',
},
kicker: {
color: '#1a5276',
fontWeight: '900',
textTransform: 'uppercase',
fontSize: 12,
marginBottom: 8,
},
title: {
fontSize: 28,
fontWeight: '900',
color: '#0f172a',
marginBottom: 18,
},
infoBox: {
backgroundColor: '#f8fafc',
padding: 14,
borderRadius: 16,
marginBottom: 12,
},
label: {
color: '#64748b',
fontWeight: '800',
fontSize: 13,
},
value: {
color: '#0f172a',
fontSize: 18,
fontWeight: '900',
marginTop: 4,
},
button: {
backgroundColor: '#1a5276',
padding: 14,
borderRadius: 14,
marginTop: 10,
},
buttonText: {
color: '#ffffff',
textAlign: 'center',
fontWeight: '900',
},
});
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.
Para que la aplicación se sienta más completa, podemos agregar algunos detalles adicionales.
En CategoriesScreen.js, antes de recorrer categorías, puedes agregar:
{categorias.length === 0 && (
<View style={styles.emptyBox}>
<Text style={styles.emptyText}>No hay categorías registradas.</Text>
</View>
)}
En ProductsScreen.js, antes de recorrer productos, puedes agregar:
{productos.length === 0 && (
<View style={styles.emptyBox}>
<Text style={styles.emptyText}>No hay productos registrados.</Text>
</View>
)}
emptyBox: {
backgroundColor: '#ffffff',
padding: 18,
borderRadius: 18,
borderWidth: 1,
borderColor: '#e2e8f0',
},
emptyText: {
color: '#64748b',
textAlign: 'center',
fontWeight: '800',
},
En algunos catch, puedes revisar el error en consola para depurar:
catch (error) {
console.log(error);
Alert.alert('Error', 'No se pudo completar la operación.');
}
Ahora es tu turno. Realiza las siguientes pruebas y mejoras.
Cambia la constante API_URL por la URL real de tu API.
Ejecuta la app y verifica que las categorías y productos se carguen desde la API.
Crea una nueva categoría y verifica en el backend que realmente se haya registrado.
Crea un nuevo producto y asígnalo a una categoría existente.
Edita un producto y verifica que los cambios se mantengan después de cerrar y abrir la app.
Apaga temporalmente la API y revisa si la app muestra un mensaje de error.
fetch.