React Native con Expo - App móvil de inventario
"Ahora la app dejará de ser solo visual y comenzará a crear, editar y eliminar datos."
En la guía anterior agregamos navegación usando Drawer, Bottom Tabs y Stack. Ahora construiremos la parte funcional de la aplicación: el CRUD local.
CRUD significa:
Crear nuevos registros.
Leer o mostrar registros existentes.
Editar registros existentes.
Eliminar registros que ya no se necesitan.
fetch.
Al finalizar esta guía, la aplicación permitirá administrar categorías y productos desde la interfaz.
TextInput.Alert para confirmar eliminaciones.Al terminar, la app tendrá dos CRUD completamente funcionales: uno para categorías y otro para productos. Los usuarios podrán crear, editar, eliminar y ver los datos en tiempo real.
| CRUD | Funcionalidad | Relación |
|---|---|---|
| Categorías | Crear, listar, editar y eliminar categorías. | Una categoría puede tener varios productos. |
| Productos | Crear, listar, editar y eliminar productos. | Cada producto pertenece a una categoría. |
Hasta este momento, los datos estaban escritos directamente dentro de algunas pantallas. Eso no es ideal, porque cada pantalla tendría su propia copia de los datos.
Para resolverlo, crearemos un contexto llamado InventoryContext. Este contexto guardará las categorías y productos en un solo lugar.
Dentro de src, crea una nueva carpeta llamada context.
mkdir src/context
Ahora crea el archivo:
Abriremos el archivo src/context/InventoryContext.js y construiremos el código poco a poco.
Primero importaremos lo necesario desde React.
import { createContext, useContext, useState } from 'react';
Estas herramientas sirven para lo siguiente:
createContext: crea el contexto.useContext: permite usar el contexto desde cualquier pantalla.useState: permite guardar y modificar los datos.Debajo de la importación, agregamos esta línea:
const InventoryContext = createContext();
Este será el contenedor general de los datos del inventario.
Ahora crearemos un componente llamado InventoryProvider.
Este componente envolverá la aplicación para compartir los datos.
export function InventoryProvider({ children }) {
return (
<InventoryContext.Provider value={{}}>
{children}
</InventoryContext.Provider>
);
}
La palabra children representa todo lo que estará dentro del provider.
En nuestro caso, será la navegación completa de la aplicación.
Dentro de la función InventoryProvider, antes del return,
agregaremos el estado de categorías.
const [categorias, setCategorias] = useState([
{
id: 1,
nombre: 'Tecnología',
descripcion: 'Equipos electrónicos y computadoras',
},
{
id: 2,
nombre: 'Accesorios',
descripcion: 'Complementos para computadoras y oficina',
},
{
id: 3,
nombre: 'Oficina',
descripcion: 'Mobiliario y herramientas de trabajo',
},
]);
Observa que ahora usamos setCategorias.
Eso nos permitirá modificar el arreglo cuando agreguemos, editemos o eliminemos categorías.
Debajo del estado de categorías, agregaremos el estado de productos.
const [productos, setProductos] = useState([
{
id: 1,
nombre: 'Laptop Dell',
precio: 850,
stock: 8,
categoriaId: 1,
},
{
id: 2,
nombre: 'Mouse inalámbrico',
precio: 18.5,
stock: 25,
categoriaId: 2,
},
{
id: 3,
nombre: 'Silla ejecutiva',
precio: 95,
stock: 4,
categoriaId: 3,
},
]);
Cada producto tiene un categoriaId. Ese dato indica a qué categoría pertenece.
Ahora construiremos la primera función del CRUD.
function agregarCategoria(nuevaCategoria) {
setCategorias([...categorias, nuevaCategoria]);
}
Esta función recibe una nueva categoría y la agrega al arreglo usando el operador spread
....
Crea un nuevo arreglo copiando las categorías existentes y agregando al final la nueva categoría.
Para editar una categoría, buscaremos la que tenga el mismo id
y la reemplazaremos.
function actualizarCategoria(categoriaActualizada) {
const nuevasCategorias = categorias.map(categoria =>
categoria.id === categoriaActualizada.id
? categoriaActualizada
: categoria
);
setCategorias(nuevasCategorias);
}
Aquí se usa map porque queremos recorrer todas las categorías.
Si encontramos la categoría que estamos editando, la reemplazamos.
Si no, la dejamos igual.
Para eliminar, usaremos filter.
function eliminarCategoria(id) {
const nuevasCategorias = categorias.filter(categoria => categoria.id !== id);
setCategorias(nuevasCategorias);
}
Esta función crea un nuevo arreglo dejando únicamente las categorías cuyo id
sea diferente al que queremos eliminar.
Ahora haremos lo mismo para los productos.
function agregarProducto(nuevoProducto) {
setProductos([...productos, nuevoProducto]);
}
function actualizarProducto(productoActualizado) {
const nuevosProductos = productos.map(producto =>
producto.id === productoActualizado.id
? productoActualizado
: producto
);
setProductos(nuevosProductos);
}
function eliminarProducto(id) {
const nuevosProductos = productos.filter(producto => producto.id !== id);
setProductos(nuevosProductos);
}
Dentro del value del provider colocaremos todo lo que queremos compartir.
const value = {
categorias,
productos,
agregarCategoria,
actualizarCategoria,
eliminarCategoria,
agregarProducto,
actualizarProducto,
eliminarProducto,
};
Luego usamos esa constante dentro del Provider.
<InventoryContext.Provider value={value}>
{children}
</InventoryContext.Provider>
Para no escribir useContext(InventoryContext) en cada pantalla,
crearemos un Hook llamado useInventory.
export function useInventory() {
return useContext(InventoryContext);
}
Este Hook nos permitirá obtener categorías, productos y funciones desde cualquier pantalla.
Después de todos los pasos anteriores, el archivo completo queda así:
import { createContext, useContext, useState } from 'react';
const InventoryContext = createContext();
export function InventoryProvider({ children }) {
const [categorias, setCategorias] = useState([
{
id: 1,
nombre: 'Tecnología',
descripcion: 'Equipos electrónicos y computadoras',
},
{
id: 2,
nombre: 'Accesorios',
descripcion: 'Complementos para computadoras y oficina',
},
{
id: 3,
nombre: 'Oficina',
descripcion: 'Mobiliario y herramientas de trabajo',
},
]);
const [productos, setProductos] = useState([
{
id: 1,
nombre: 'Laptop Dell',
precio: 850,
stock: 8,
categoriaId: 1,
},
{
id: 2,
nombre: 'Mouse inalámbrico',
precio: 18.5,
stock: 25,
categoriaId: 2,
},
{
id: 3,
nombre: 'Silla ejecutiva',
precio: 95,
stock: 4,
categoriaId: 3,
},
]);
function agregarCategoria(nuevaCategoria) {
setCategorias([...categorias, nuevaCategoria]);
}
function actualizarCategoria(categoriaActualizada) {
const nuevasCategorias = categorias.map(categoria =>
categoria.id === categoriaActualizada.id
? categoriaActualizada
: categoria
);
setCategorias(nuevasCategorias);
}
function eliminarCategoria(id) {
const nuevasCategorias = categorias.filter(categoria => categoria.id !== id);
setCategorias(nuevasCategorias);
}
function agregarProducto(nuevoProducto) {
setProductos([...productos, nuevoProducto]);
}
function actualizarProducto(productoActualizado) {
const nuevosProductos = productos.map(producto =>
producto.id === productoActualizado.id
? productoActualizado
: producto
);
setProductos(nuevosProductos);
}
function eliminarProducto(id) {
const nuevosProductos = productos.filter(producto => producto.id !== id);
setProductos(nuevosProductos);
}
const value = {
categorias,
productos,
agregarCategoria,
actualizarCategoria,
eliminarCategoria,
agregarProducto,
actualizarProducto,
eliminarProducto,
};
return (
<InventoryContext.Provider value={value}>
{children}
</InventoryContext.Provider>
);
}
export function useInventory() {
return useContext(InventoryContext);
}
Ya creamos el contexto, pero todavía no lo hemos conectado con la aplicación. Para hacerlo, debemos modificar el archivo App.js.
En App.js, agrega esta importación:
import { InventoryProvider } from './src/context/InventoryContext';
Actualmente tenemos algo parecido a esto:
<NavigationContainer>
<RootDrawer />
</NavigationContainer>
Ahora colocaremos InventoryProvider alrededor del contenedor de navegación.
<InventoryProvider>
<NavigationContainer>
<RootDrawer />
</NavigationContainer>
</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>
);
}
useInventory().
En la Guía 1, HomeScreen tenía sus propios arreglos de categorías y productos.
Ahora esos datos vendrán desde el contexto.
En HomeScreen.js, elimina esta importación:
import { useState } from 'react';
Agrega esta importación:
import { useInventory } from '../context/InventoryContext';
Dentro de la función HomeScreen, agrega:
const { categorias, productos } = useInventory();
Ahora la pantalla ya no necesita tener datos propios. Usará los datos compartidos por el contexto.
Estos cálculos pueden quedarse casi igual:
const totalCategorias = categorias.length;
const totalProductos = productos.length;
const productosBajoStock = productos.filter(producto => producto.stock <= 5).length;
También podemos conservar esta función:
function obtenerNombreCategoria(categoriaId) {
const categoriaEncontrada = categorias.find(
categoria => categoria.id === categoriaId
);
return categoriaEncontrada ? categoriaEncontrada.nombre : 'Sin categoría';
}
Si más adelante agregamos un producto desde la pantalla de productos, el inicio también podrá reflejar esa información porque ambos leen del mismo contexto.
Ahora modificaremos CategoriesScreen.js. Esta pantalla permitirá crear, listar, editar y eliminar categorías.
Usaremos useState para controlar los campos del formulario.
import { useState } from 'react';
Necesitaremos TextInput, Pressable y Alert.
import {
Alert,
ScrollView,
View,
Text,
TextInput,
Pressable,
StyleSheet,
} from 'react-native';
Desde el contexto obtendremos categorías y funciones del CRUD.
import { useInventory } from '../context/InventoryContext';
Dentro de la función CategoriesScreen, escribimos:
const {
categorias,
productos,
agregarCategoria,
actualizarCategoria,
eliminarCategoria,
} = useInventory();
También obtenemos productos porque no deberíamos eliminar una categoría
si ya tiene productos asignados.
El formulario tendrá dos campos: nombre y descripción.
const [nombre, setNombre] = useState('');
const [descripcion, setDescripcion] = useState('');
const [categoriaEditandoId, setCategoriaEditandoId] = useState(null);
La variable categoriaEditandoId nos ayudará a saber si estamos creando
una categoría nueva o editando una existente.
Después de guardar o cancelar, necesitaremos limpiar los campos.
function limpiarFormulario() {
setNombre('');
setDescripcion('');
setCategoriaEditandoId(null);
}
Paso 7.1: Validar entrada
Primero verificamos que el usuario escribió algo en el nombre:
function guardarCategoria() {
if (nombre.trim() === '') {
Alert.alert('Campo requerido', 'Escribe el nombre de la categoría.');
return;
}
}
Si el nombre está vacío, mostramos una alerta y salimos de la función.
Paso 7.2: Crear el objeto de categoría
Después de validar, construimos el objeto que guardaremos:
const categoria = {
id: categoriaEditandoId ? categoriaEditandoId : Date.now(),
nombre: nombre.trim(),
descripcion: descripcion.trim(),
};
Nota: Si estamos editando, usamos el ID existente. Si es nueva, Date.now() genera un ID único.
Después de crear el objeto, verificamos si estamos editando o creando:
if (categoriaEditandoId) {
actualizarCategoria(categoria);
} else {
agregarCategoria(categoria);
}
limpiarFormulario();
Si categoriaEditandoId tiene valor, estamos editando.
Si es nulo, es una categoría nueva.
Al final, limpiamos el formulario para que el usuario pueda crear otra.
Cuando el usuario toque el botón editar, llenaremos el formulario con los datos actuales.
function editarCategoria(categoria) {
setNombre(categoria.nombre);
setDescripcion(categoria.descripcion);
setCategoriaEditandoId(categoria.id);
}
Primero verificaremos si hay productos usando esa categoría.
const tieneProductos = productos.some(
producto => producto.categoriaId === id
);
Si tiene productos, mostraremos una alerta y no eliminaremos la categoría.
if (tieneProductos) {
Alert.alert(
'No se puede eliminar',
'Esta categoría tiene productos asociados.'
);
return;
}
Luego pediremos confirmación.
Alert.alert(
'Eliminar categoría',
'¿Deseas eliminar esta categoría?',
[
{
text: 'Cancelar',
style: 'cancel',
},
{
text: 'Eliminar',
style: 'destructive',
onPress: () => eliminarCategoria(id),
},
]
);
Dentro del return, agregaremos una tarjeta para el formulario.
<View style={styles.formCard}>
<Text style={styles.formTitle}>
{categoriaEditandoId ? 'Editar categoría' : 'Nueva categoría'}
</Text>
</View>
Ahora agregaremos el primer campo de texto.
<TextInput
style={styles.input}
placeholder="Nombre de la categoría"
value={nombre}
onChangeText={setNombre}
/>
Agregamos el segundo campo.
<TextInput
style={[styles.input, styles.textArea]}
placeholder="Descripción"
value={descripcion}
onChangeText={setDescripcion}
multiline
/>
Finalmente, agregamos el botón para guardar.
<Pressable style={styles.primaryButton} onPress={guardarCategoria}>
<Text style={styles.primaryButtonText}>
{categoriaEditandoId ? 'Actualizar categoría' : 'Guardar categoría'}
</Text>
</Pressable>
Cuando estemos editando, también mostraremos un botón para cancelar.
{categoriaEditandoId && (
<Pressable style={styles.cancelButton} onPress={limpiarFormulario}>
<Text style={styles.cancelButtonText}>Cancelar edición</Text>
</Pressable>
)}
Debajo del formulario, mostraremos las categorías con su estructura básica.
Paso 12.1: Estructura básica de la lista
{categorias.map(categoria => (
<View key={categoria.id} style={styles.card}>
<Text style={styles.categoryName}>{categoria.nombre}</Text>
<Text style={styles.description}>{categoria.descripcion}</Text>
</View>
))}
En este punto, cada categoría se muestra en una tarjeta con su nombre y descripción.
Paso 12.2: Agregar botones de acciones
Ahora agregamos botones de editar y eliminar dentro de cada tarjeta.
<View style={styles.actionsRow}>
<Pressable
style={styles.editButton}
onPress={() => editarCategoria(categoria)}
>
<Text style={styles.actionText}>Editar</Text>
</Pressable>
<Pressable
style={styles.deleteButton}
onPress={() => confirmarEliminarCategoria(categoria.id)}
>
<Text style={styles.actionText}>Eliminar</Text>
</Pressable>
</View>
import { useState } from 'react';
import {
Alert,
ScrollView,
View,
Text,
TextInput,
Pressable,
StyleSheet,
} from 'react-native';
import { useInventory } from '../context/InventoryContext';
export default function CategoriesScreen() {
const {
categorias,
productos,
agregarCategoria,
actualizarCategoria,
eliminarCategoria,
} = useInventory();
const [nombre, setNombre] = useState('');
const [descripcion, setDescripcion] = useState('');
const [categoriaEditandoId, setCategoriaEditandoId] = useState(null);
function limpiarFormulario() {
setNombre('');
setDescripcion('');
setCategoriaEditandoId(null);
}
function guardarCategoria() {
if (nombre.trim() === '') {
Alert.alert('Campo requerido', 'Escribe el nombre de la categoría.');
return;
}
const categoria = {
id: categoriaEditandoId ? categoriaEditandoId : Date.now(),
nombre: nombre.trim(),
descripcion: descripcion.trim(),
};
if (categoriaEditandoId) {
actualizarCategoria(categoria);
} else {
agregarCategoria(categoria);
}
limpiarFormulario();
}
function editarCategoria(categoria) {
setNombre(categoria.nombre);
setDescripcion(categoria.descripcion);
setCategoriaEditandoId(categoria.id);
}
function confirmarEliminarCategoria(id) {
const tieneProductos = productos.some(
producto => producto.categoriaId === id
);
if (tieneProductos) {
Alert.alert(
'No se puede eliminar',
'Esta categoría tiene productos asociados.'
);
return;
}
Alert.alert(
'Eliminar categoría',
'¿Deseas eliminar esta categoría?',
[
{
text: 'Cancelar',
style: 'cancel',
},
{
text: 'Eliminar',
style: 'destructive',
onPress: () => eliminarCategoria(id),
},
]
);
}
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Categorías</Text>
<Text style={styles.subtitle}>
Crea, edita y elimina categorías del inventario.
</Text>
<View style={styles.formCard}>
<Text style={styles.formTitle}>
{categoriaEditandoId ? 'Editar categoría' : 'Nueva categoría'}
</Text>
<TextInput
style={styles.input}
placeholder="Nombre de la categoría"
value={nombre}
onChangeText={setNombre}
/>
<TextInput
style={[styles.input, styles.textArea]}
placeholder="Descripción"
value={descripcion}
onChangeText={setDescripcion}
multiline
/>
<Pressable style={styles.primaryButton} onPress={guardarCategoria}>
<Text style={styles.primaryButtonText}>
{categoriaEditandoId ? 'Actualizar categoría' : 'Guardar categoría'}
</Text>
</Pressable>
{categoriaEditandoId && (
<Pressable style={styles.cancelButton} onPress={limpiarFormulario}>
<Text style={styles.cancelButtonText}>Cancelar edición</Text>
</Pressable>
)}
</View>
{categorias.map(categoria => (
<View key={categoria.id} style={styles.card}>
<Text style={styles.categoryName}>{categoria.nombre}</Text>
<Text style={styles.description}>{categoria.descripcion}</Text>
<View style={styles.actionsRow}>
<Pressable
style={styles.editButton}
onPress={() => editarCategoria(categoria)}
>
<Text style={styles.actionText}>Editar</Text>
</Pressable>
<Pressable
style={styles.deleteButton}
onPress={() => confirmarEliminarCategoria(categoria.id)}
>
<Text style={styles.actionText}>Eliminar</Text>
</Pressable>
</View>
</View>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 20,
paddingBottom: 40,
},
title: {
fontSize: 28,
fontWeight: '900',
color: '#0f172a',
},
subtitle: {
color: '#64748b',
marginTop: 6,
marginBottom: 18,
fontSize: 15,
},
formCard: {
backgroundColor: '#ffffff',
padding: 18,
borderRadius: 18,
marginBottom: 18,
borderWidth: 1,
borderColor: '#e2e8f0',
},
formTitle: {
fontSize: 18,
fontWeight: '900',
color: '#0f172a',
marginBottom: 12,
},
input: {
backgroundColor: '#f8fafc',
borderWidth: 1,
borderColor: '#e2e8f0',
borderRadius: 14,
padding: 12,
marginBottom: 10,
color: '#0f172a',
},
textArea: {
minHeight: 80,
textAlignVertical: 'top',
},
primaryButton: {
backgroundColor: '#1a5276',
padding: 14,
borderRadius: 14,
marginTop: 4,
},
primaryButtonText: {
color: '#ffffff',
textAlign: 'center',
fontWeight: '900',
},
cancelButton: {
backgroundColor: '#e2e8f0',
padding: 14,
borderRadius: 14,
marginTop: 10,
},
cancelButtonText: {
color: '#0f172a',
textAlign: 'center',
fontWeight: '900',
},
card: {
backgroundColor: '#ffffff',
padding: 18,
borderRadius: 18,
marginBottom: 12,
borderWidth: 1,
borderColor: '#e2e8f0',
},
categoryName: {
fontSize: 18,
fontWeight: '900',
color: '#1a5276',
},
description: {
color: '#64748b',
marginTop: 5,
},
actionsRow: {
flexDirection: 'row',
gap: 10,
marginTop: 14,
},
editButton: {
flex: 1,
backgroundColor: '#e0f2fe',
padding: 12,
borderRadius: 12,
},
deleteButton: {
flex: 1,
backgroundColor: '#fee2e2',
padding: 12,
borderRadius: 12,
},
actionText: {
textAlign: 'center',
fontWeight: '900',
color: '#0f172a',
},
});
Después de completar todos los pasos anteriores, tu pantalla de categorías se verá así:
Nueva categoría
Tecnología
Equipos electrónicos y computadoras
Accesorios
Complementos para computadoras y oficina
En la pantalla puedes ver:
Ahora modificaremos ProductsScreen.js. Esta pantalla permitirá crear, editar y eliminar productos.
Además, cada producto deberá relacionarse con una categoría existente.
import { useState } from 'react';
import {
Alert,
ScrollView,
View,
Text,
TextInput,
Pressable,
StyleSheet,
} from 'react-native';
import ProductCard from '../components/ProductCard';
import { useInventory } from '../context/InventoryContext';
Dentro de ProductsScreen, agregamos:
const {
categorias,
productos,
agregarProducto,
actualizarProducto,
eliminarProducto,
} = useInventory();
El producto tendrá nombre, precio, stock y categoría.
const [nombre, setNombre] = useState('');
const [precio, setPrecio] = useState('');
const [stock, setStock] = useState('');
const [categoriaId, setCategoriaId] = useState(null);
const [productoEditandoId, setProductoEditandoId] = useState(null);
Usamos categoriaId para guardar la categoría seleccionada.
Para evitar que la categoría esté vacía, podemos usar la primera categoría disponible.
const categoriaSeleccionadaId = categoriaId || categorias[0]?.id;
El signo ?. evita errores si todavía no hay categorías creadas.
function limpiarFormulario() {
setNombre('');
setPrecio('');
setStock('');
setCategoriaId(null);
setProductoEditandoId(null);
}
Paso 8.1: Validar que el nombre no esté vacío
if (nombre.trim() === '') {
Alert.alert('Campo requerido', 'Escribe el nombre del producto.');
return;
}
Paso 8.2: Convertir precio y stock a números
Los inputs de texto necesitan ser convertidos a números:
const precioNumero = parseFloat(precio);
const stockNumero = parseInt(stock, 10);
Paso 8.3: Validar que precio y stock sean válidos
Verificamos que sean números reales y positivos:
if (isNaN(precioNumero) || precioNumero <= 0) {
Alert.alert('Precio inválido', 'Escribe un precio válido.');
return;
}
if (isNaN(stockNumero) || stockNumero < 0) {
Alert.alert('Stock inválido', 'Escribe una cantidad válida.');
return;
}
isNaN() verifica si el valor NO es un número.
Para el precio, debe ser positivo (>0).
Para el stock, puede ser 0 pero no negativo.
Un producto debe pertenecer a una categoría.
if (!categoriaSeleccionadaId) {
Alert.alert('Categoría requerida', 'Primero debes crear una categoría.');
return;
}
Con todos los datos validados, creamos el objeto producto:
const producto = {
id: productoEditandoId ? productoEditandoId : Date.now(),
nombre: nombre.trim(),
precio: precioNumero,
stock: stockNumero,
categoriaId: categoriaSeleccionadaId,
};
Nota que usamos los números ya convertidos (precioNumero, stockNumero)
en lugar de los strings originales.
Similar a categorías, verificamos si estamos editando o creando:
if (productoEditandoId) {
actualizarProducto(producto);
} else {
agregarProducto(producto);
}
limpiarFormulario();
Si estamos editando, actualiza. Si no, agrega como nuevo. Al final, limpia el formulario.
Esta función colocará los datos del producto en el formulario.
function editarProducto(producto) {
setNombre(producto.nombre);
setPrecio(String(producto.precio));
setStock(String(producto.stock));
setCategoriaId(producto.categoriaId);
setProductoEditandoId(producto.id);
}
Para eliminar productos, pediremos confirmación.
function confirmarEliminarProducto(id) {
Alert.alert(
'Eliminar producto',
'¿Deseas eliminar este producto?',
[
{
text: 'Cancelar',
style: 'cancel',
},
{
text: 'Eliminar',
style: 'destructive',
onPress: () => eliminarProducto(id),
},
]
);
}
function obtenerNombreCategoria(id) {
const categoriaEncontrada = categorias.find(
categoria => categoria.id === id
);
return categoriaEncontrada ? categoriaEncontrada.nombre : 'Sin categoría';
}
Primero creamos la tarjeta del formulario.
<View style={styles.formCard}>
<Text style={styles.formTitle}>
{productoEditandoId ? 'Editar producto' : 'Nuevo producto'}
</Text>
</View>
Ahora agregamos el campo del nombre.
<TextInput
style={styles.input}
placeholder="Nombre del producto"
value={nombre}
onChangeText={setNombre}
/>
Agregamos el campo del precio.
<TextInput
style={styles.input}
placeholder="Precio"
value={precio}
onChangeText={setPrecio}
keyboardType="decimal-pad"
/>
Agregamos el campo del stock.
<TextInput
style={styles.input}
placeholder="Stock"
value={stock}
onChangeText={setStock}
keyboardType="number-pad"
/>
En lugar de usar una lista desplegable, usaremos botones tipo etiqueta. Cada categoría será un botón seleccionable.
<Text style={styles.label}>Categoría</Text>
Ahora recorremos las categorías:
<View style={styles.categoryRow}>
{categorias.map(categoria => (
<Pressable
key={categoria.id}
style={[
styles.categoryChip,
categoriaSeleccionadaId === categoria.id && styles.categoryChipActive,
]}
onPress={() => setCategoriaId(categoria.id)}
>
<Text
style={[
styles.categoryChipText,
categoriaSeleccionadaId === categoria.id && styles.categoryChipTextActive,
]}
>
{categoria.nombre}
</Text>
</Pressable>
))}
</View>
Cuando una categoría está seleccionada, se aplican estilos adicionales.
<Pressable style={styles.primaryButton} onPress={guardarProducto}>
<Text style={styles.primaryButtonText}>
{productoEditandoId ? 'Actualizar producto' : 'Guardar producto'}
</Text>
</Pressable>
{productoEditandoId && (
<Pressable style={styles.cancelButton} onPress={limpiarFormulario}>
<Text style={styles.cancelButtonText}>Cancelar edición</Text>
</Pressable>
)}
Debajo del formulario mostraremos los productos.
{productos.map(producto => (
<ProductCard
key={producto.id}
producto={producto}
categoria={obtenerNombreCategoria(producto.categoriaId)}
onPress={() =>
navigation.navigate('DetalleProducto', {
producto: producto,
categoria: obtenerNombreCategoria(producto.categoriaId),
})
}
onEdit={() => editarProducto(producto)}
onDelete={() => confirmarEliminarProducto(producto.id)}
/>
))}
Observa que ahora ProductCard recibirá tres acciones:
onPress: abrir detalle.onEdit: editar producto.onDelete: eliminar producto.import { useState } from 'react';
import {
Alert,
ScrollView,
View,
Text,
TextInput,
Pressable,
StyleSheet,
} from 'react-native';
import ProductCard from '../components/ProductCard';
import { useInventory } from '../context/InventoryContext';
export default function ProductsScreen({ navigation }) {
const {
categorias,
productos,
agregarProducto,
actualizarProducto,
eliminarProducto,
} = useInventory();
const [nombre, setNombre] = useState('');
const [precio, setPrecio] = useState('');
const [stock, setStock] = useState('');
const [categoriaId, setCategoriaId] = useState(null);
const [productoEditandoId, setProductoEditandoId] = useState(null);
const categoriaSeleccionadaId = categoriaId || categorias[0]?.id;
function limpiarFormulario() {
setNombre('');
setPrecio('');
setStock('');
setCategoriaId(null);
setProductoEditandoId(null);
}
function guardarProducto() {
if (nombre.trim() === '') {
Alert.alert('Campo requerido', 'Escribe el nombre del producto.');
return;
}
const precioNumero = parseFloat(precio);
const stockNumero = parseInt(stock, 10);
if (isNaN(precioNumero) || precioNumero <= 0) {
Alert.alert('Precio inválido', 'Escribe un precio válido.');
return;
}
if (isNaN(stockNumero) || stockNumero < 0) {
Alert.alert('Stock inválido', 'Escribe una cantidad válida.');
return;
}
if (!categoriaSeleccionadaId) {
Alert.alert('Categoría requerida', 'Primero debes crear una categoría.');
return;
}
const producto = {
id: productoEditandoId ? productoEditandoId : Date.now(),
nombre: nombre.trim(),
precio: precioNumero,
stock: stockNumero,
categoriaId: categoriaSeleccionadaId,
};
if (productoEditandoId) {
actualizarProducto(producto);
} else {
agregarProducto(producto);
}
limpiarFormulario();
}
function editarProducto(producto) {
setNombre(producto.nombre);
setPrecio(String(producto.precio));
setStock(String(producto.stock));
setCategoriaId(producto.categoriaId);
setProductoEditandoId(producto.id);
}
function confirmarEliminarProducto(id) {
Alert.alert(
'Eliminar producto',
'¿Deseas eliminar este producto?',
[
{
text: 'Cancelar',
style: 'cancel',
},
{
text: 'Eliminar',
style: 'destructive',
onPress: () => eliminarProducto(id),
},
]
);
}
function obtenerNombreCategoria(id) {
const categoriaEncontrada = categorias.find(
categoria => categoria.id === id
);
return categoriaEncontrada ? categoriaEncontrada.nombre : 'Sin categoría';
}
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Productos</Text>
<Text style={styles.subtitle}>
Crea, edita y elimina productos del inventario.
</Text>
<View style={styles.formCard}>
<Text style={styles.formTitle}>
{productoEditandoId ? 'Editar producto' : 'Nuevo producto'}
</Text>
<TextInput
style={styles.input}
placeholder="Nombre del producto"
value={nombre}
onChangeText={setNombre}
/>
<TextInput
style={styles.input}
placeholder="Precio"
value={precio}
onChangeText={setPrecio}
keyboardType="decimal-pad"
/>
<TextInput
style={styles.input}
placeholder="Stock"
value={stock}
onChangeText={setStock}
keyboardType="number-pad"
/>
<Text style={styles.label}>Categoría</Text>
<View style={styles.categoryRow}>
{categorias.map(categoria => (
<Pressable
key={categoria.id}
style={[
styles.categoryChip,
categoriaSeleccionadaId === categoria.id && styles.categoryChipActive,
]}
onPress={() => setCategoriaId(categoria.id)}
>
<Text
style={[
styles.categoryChipText,
categoriaSeleccionadaId === categoria.id && styles.categoryChipTextActive,
]}
>
{categoria.nombre}
</Text>
</Pressable>
))}
</View>
<Pressable style={styles.primaryButton} onPress={guardarProducto}>
<Text style={styles.primaryButtonText}>
{productoEditandoId ? 'Actualizar producto' : 'Guardar producto'}
</Text>
</Pressable>
{productoEditandoId && (
<Pressable style={styles.cancelButton} onPress={limpiarFormulario}>
<Text style={styles.cancelButtonText}>Cancelar edición</Text>
</Pressable>
)}
</View>
{productos.map(producto => (
<ProductCard
key={producto.id}
producto={producto}
categoria={obtenerNombreCategoria(producto.categoriaId)}
onPress={() =>
navigation.navigate('DetalleProducto', {
producto: producto,
categoria: obtenerNombreCategoria(producto.categoriaId),
})
}
onEdit={() => editarProducto(producto)}
onDelete={() => confirmarEliminarProducto(producto.id)}
/>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 20,
paddingBottom: 40,
},
title: {
fontSize: 28,
fontWeight: '900',
color: '#0f172a',
},
subtitle: {
color: '#64748b',
marginTop: 6,
marginBottom: 18,
fontSize: 15,
},
formCard: {
backgroundColor: '#ffffff',
padding: 18,
borderRadius: 18,
marginBottom: 18,
borderWidth: 1,
borderColor: '#e2e8f0',
},
formTitle: {
fontSize: 18,
fontWeight: '900',
color: '#0f172a',
marginBottom: 12,
},
input: {
backgroundColor: '#f8fafc',
borderWidth: 1,
borderColor: '#e2e8f0',
borderRadius: 14,
padding: 12,
marginBottom: 10,
color: '#0f172a',
},
label: {
fontWeight: '900',
color: '#0f172a',
marginBottom: 8,
},
categoryRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginBottom: 14,
},
categoryChip: {
backgroundColor: '#f1f5f9',
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 999,
},
categoryChipActive: {
backgroundColor: '#1a5276',
},
categoryChipText: {
color: '#64748b',
fontWeight: '900',
},
categoryChipTextActive: {
color: '#ffffff',
},
primaryButton: {
backgroundColor: '#1a5276',
padding: 14,
borderRadius: 14,
marginTop: 4,
},
primaryButtonText: {
color: '#ffffff',
textAlign: 'center',
fontWeight: '900',
},
cancelButton: {
backgroundColor: '#e2e8f0',
padding: 14,
borderRadius: 14,
marginTop: 10,
},
cancelButtonText: {
color: '#0f172a',
textAlign: 'center',
fontWeight: '900',
},
});
Después de completar todos los pasos del CRUD de productos, tu pantalla se verá así:
Nuevo producto
Productos
Laptop Dell
$850.00 - Stock: 8
TecnologíaMouse inalámbrico
$18.50 - Stock: 25
AccesoriosEn la pantalla puedes ver:
El componente ProductCard ahora debe mostrar botones para editar y eliminar.
Antes teníamos algo parecido a esto:
export default function ProductCard({ producto, categoria, onPress }) {
Ahora recibiremos también onEdit y onDelete.
export default function ProductCard({ producto, categoria, onPress, onEdit, onDelete }) {
Debajo de la información del producto, agregaremos:
<View style={styles.actionsRow}>
<Pressable style={styles.editButton} onPress={onEdit}>
<Text style={styles.actionText}>Editar</Text>
</Pressable>
<Pressable style={styles.deleteButton} onPress={onDelete}>
<Text style={styles.actionText}>Eliminar</Text>
</Pressable>
</View>
import { Pressable, View, Text, StyleSheet } from 'react-native';
import CategoryPill from './CategoryPill';
export default function ProductCard({ producto, categoria, onPress, onEdit, onDelete }) {
return (
<View style={styles.card}>
<Pressable onPress={onPress}>
<View style={styles.row}>
<View style={styles.info}>
<Text style={styles.nombre}>{producto.nombre}</Text>
<Text style={styles.detalle}>
Precio: ${producto.precio.toFixed(2)}
</Text>
<Text style={styles.detalle}>
Stock: {producto.stock} unidades
</Text>
<CategoryPill nombre={categoria} />
</View>
<View style={styles.badge}>
<Text style={styles.badgeText}>
{producto.stock <= 5 ? 'Bajo' : 'OK'}
</Text>
</View>
</View>
</Pressable>
<View style={styles.actionsRow}>
<Pressable style={styles.editButton} onPress={onEdit}>
<Text style={styles.actionText}>Editar</Text>
</Pressable>
<Pressable style={styles.deleteButton} onPress={onDelete}>
<Text style={styles.actionText}>Eliminar</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#ffffff',
padding: 16,
borderRadius: 18,
marginBottom: 12,
borderWidth: 1,
borderColor: '#e2e8f0',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 12,
},
info: {
flex: 1,
},
nombre: {
fontSize: 17,
fontWeight: '900',
color: '#0f172a',
},
detalle: {
color: '#64748b',
marginTop: 3,
},
badge: {
backgroundColor: '#e0f2fe',
borderRadius: 12,
paddingHorizontal: 10,
paddingVertical: 6,
alignSelf: 'flex-start',
},
badgeText: {
color: '#0369a1',
fontWeight: '900',
fontSize: 12,
},
actionsRow: {
flexDirection: 'row',
gap: 10,
marginTop: 14,
},
editButton: {
flex: 1,
backgroundColor: '#e0f2fe',
padding: 12,
borderRadius: 12,
},
deleteButton: {
flex: 1,
backgroundColor: '#fee2e2',
padding: 12,
borderRadius: 12,
},
actionText: {
textAlign: 'center',
fontWeight: '900',
color: '#0f172a',
},
});
La pantalla de detalle puede mantenerse igual que en la Guía 2. Sin embargo, agregaremos una pequeña validación para evitar errores si la pantalla se abre sin datos.
Después de recibir route, podemos agregar:
if (!route.params) {
return (
<View style={styles.container}>
<Text>No se recibió información del producto.</Text>
</View>
);
}
const { producto, categoria } = route.params;
En esta guía el detalle muestra la información que se envió al navegar. En la siguiente guía, cuando trabajemos con API, podremos consultar datos más actualizados.
Ahora es tu turno. Realiza los siguientes cambios en la aplicación.
Agrega una nueva categoría desde la app y verifica que aparezca en la lista.
Edita una categoría existente y cambia su descripción.
Intenta eliminar una categoría que tenga productos relacionados. Verifica que la app muestre una alerta y no permita eliminarla.
Crea un nuevo producto y asígnalo a una categoría existente.
Edita el precio y el stock de un producto.
Elimina un producto y verifica que desaparezca de la lista.
fetch.
El CRUD dejará de ser solamente local y comenzará a comunicarse con endpoints reales.