¿Qué es React?
Una librería JavaScript para construir interfaces de usuario a partir de componentes reutilizables. No es un framework completo — es deliberadamente pequeño y enfocado en la capa de vista.
Creado por Meta en 2013, React cambió la forma de pensar en interfaces web. En lugar de manipular el DOM directamente (con jQuery o JavaScript puro), React te permite describir cómo debe verse la UI y deja que la librería actualice el DOM de la forma más eficiente posible.
JSX y el Virtual DOM
JSX es una extensión de JavaScript que permite escribir HTML dentro de JS. Babel lo transforma en llamadas a React.createElement().
// ✍️ Lo que escribes en tu editor: const elemento = ( <div className="tarjeta"> <h1>Hola, {nombre}</h1> <p>Bienvenido a React</p> </div> ); // ⚙️ Lo que Babel genera (automáticamente): const elemento = React.createElement( 'div', { className: 'tarjeta' }, React.createElement('h1', null, 'Hola, ', nombre), React.createElement('p', null, 'Bienvenido a React') );
Reglas de JSX:
// 1. Un solo elemento raíz (o Fragment) return ( <> // Fragment ← no genera HTML <h1>Título</h1> <p>Párrafo</p> </> ); // 2. className en lugar de class <div className="contenedor">...</div> // 3. Expresiones JS entre llaves {} <p>Total: {precio * cantidad}</p> <p>Fecha: {new Date().toLocaleDateString('es-MX')}</p> // 4. Renderizado condicional {isLoading && <Spinner />} {error ? <Error msg={error} /> : <Datos />} // 5. Atributos en camelCase <input type="text" onChange={handleChange} autoFocus />
Componentes Funcionales
Un componente es simplemente una función que recibe datos (props) y devuelve JSX. Es la unidad fundamental de React moderno.
// Componente funcional — la forma moderna (React 16.8+) function TarjetaProducto({ nombre, precio, stock, imagen }) { const agotado = stock === 0; return ( <div className={`tarjeta ${agotado ? 'agotado' : ''}`}> <img src={imagen} alt={nombre} /> <h3>{nombre}</h3> <p>{formatearPeso(precio)}</p> {agotado ? <span className="badge rojo">Agotado</span> : <button>Agregar al carrito</button> } </div> ); } // Uso del componente (como si fuera una etiqueta HTML) <TarjetaProducto nombre="Arrachera Premium" precio={189.50} stock={12} imagen="/img/arrachera.jpg" />
TarjetaProducto). React usa esto para distinguir entre etiquetas HTML nativas (<div>) y componentes propios (<TarjetaProducto>). Un componente con minúscula sería interpretado como etiqueta HTML.
Props — Datos de Entrada
Las props (properties) son la forma en que los componentes reciben datos del exterior. Son de solo lectura — un componente nunca modifica sus propias props.
// Props con desestructuración + valores por defecto function Boton({ children, // contenido entre etiquetas variante = 'primario', // default value tamaño = 'md', disabled = false, onClick, // función callback }) { return ( <button className={`btn btn-${variante} btn-${tamaño}`} disabled={disabled} onClick={onClick} > {children} </button> ); } // Diferentes usos del mismo componente: <Boton>Guardar</Boton> <Boton variante="peligro" onClick={eliminar}>Eliminar</Boton> <Boton disabled>Procesando...</Boton>
useState — Estado Local
El hook fundamental. Permite que un componente "recuerde" valores entre renders. Cuando el estado cambia, React re-renderiza el componente automáticamente.
import { useState } from 'react'; function Contador() { // [valor actual] [función actualizadora] [valor inicial] const [cuenta, setCuenta] = useState(0); // Actualización directa const incrementar = () => setCuenta(cuenta + 1); // Actualización funcional (recomendada cuando depende del valor previo) const incrementarSeguro = () => setCuenta(prev => prev + 1); return ( <> <p>Cuenta: {cuenta}</p> <button onClick={incrementarSeguro}>+</button> <button onClick={() => setCuenta(0)}>Reset</button> </> ); } // Estado con objetos — siempre copia, nunca mutas const [usuario, setUsuario] = useState({ nombre: '', email: '' }); // ✅ Correcto: setUsuario(prev => ({ ...prev, nombre: 'Ana' })); // ❌ Incorrecto (mutación directa no dispara re-render): // usuario.nombre = 'Ana';
setState(newValue) o el patrón funcional setState(prev => ...).
useEffect — Efectos Secundarios
Para operaciones que ocurren "fuera" del renderizado: fetch de datos, subscripciones a eventos, manipulación del DOM, timers.
import { useState, useEffect } from 'react'; function PerfilUsuario({ userId }) { const [usuario, setUsuario] = useState(null); const [loading, setLoading] = useState(true); // ── Patrón 1: Sin array de dependencias ─────────────── // Corre después de CADA render useEffect(() => { document.title = `Perfil: ${usuario?.nombre}`; }); // ── Patrón 2: Array vacío [] ────────────────────────── // Corre solo UNA VEZ al montar el componente useEffect(() => { console.log('Componente montado'); return () => console.log('Componente desmontado (cleanup)'); }, []); // ── Patrón 3: Con dependencias [dep1, dep2] ─────────── // Corre cuando userId cambia useEffect(() => { let cancelado = false; // evitar race conditions setLoading(true); fetch(`/api/usuarios/${userId}`) .then(r => r.json()) .then(data => { if (!cancelado) { setUsuario(data); setLoading(false); } }); return () => { cancelado = true; }; // cleanup }, [userId]); // ← vuelve a correr si userId cambia if (loading) return <Spinner />; return <div>{usuario.nombre}</div>; }
useRef — Referencias y Valores Persistentes
useRef tiene dos usos: acceder a elementos del DOM directamente, y guardar valores que persisten entre renders sin disparar un re-render.
import { useRef, useEffect, useState } from 'react'; // ── Uso 1: Referencia al DOM ────────────────────────── function CampoConFocus() { const inputRef = useRef(null); useEffect(() => { inputRef.current.focus(); // foco automático al montar }, []); const limpiar = () => { inputRef.current.value = ''; inputRef.current.focus(); }; return <input ref={inputRef} placeholder="Buscar producto..." />; } // ── Uso 2: Valor persistente sin re-render ──────────── function Cronometro() { const [tiempo, setTiempo] = useState(0); const intervalRef = useRef(null); // guarda el ID del interval const iniciar = () => { intervalRef.current = setInterval(() => { setTiempo(t => t + 1); }, 1000); }; const detener = () => clearInterval(intervalRef.current); return ( <> <p>{tiempo}s</p> <button onClick={iniciar}>Iniciar</button> <button onClick={detener}>Detener</button> </> ); }
useContext — Estado Global Simple
Context API evita "prop drilling" — pasar props por múltiples niveles de componentes solo para que lleguen al fondo. Ideal para tema, usuario autenticado, idioma.
import { createContext, useContext, useState } from 'react'; // 1. Crear el contexto const AuthContext = createContext(null); // 2. Crear el Provider (envuelve la app o sección que necesita acceso) export function AuthProvider({ children }) { const [usuario, setUsuario] = useState(null); const login = async (email, password) => { const data = await fetchLogin(email, password); setUsuario(data); }; const logout = () => setUsuario(null); return ( <AuthContext.Provider value={{ usuario, login, logout }}> {children} </AuthContext.Provider> ); } // 3. Hook personalizado para consumir el contexto export function useAuth() { const ctx = useContext(AuthContext); if (!ctx) throw new Error('useAuth debe usarse dentro de AuthProvider'); return ctx; } // 4. Uso en cualquier componente anidado (sin pasar props) function Header() { const { usuario, logout } = useAuth(); return ( <header> <span>Hola, {usuario.nombre}</span> <button onClick={logout}>Cerrar sesión</button> </header> ); }
useMemo y useCallback
Optimización de performance: evitar recalcular valores costosos o recrear funciones en cada render. Úsalos solo cuando hay un problema real de rendimiento.
import { useMemo, useCallback } from 'react'; function ReporteVentas({ ventas, filtro }) { // useMemo: memoriza el resultado de un cálculo costoso // Solo recalcula si [ventas, filtro] cambian const ventasFiltradas = useMemo(() => { return ventas .filter(v => v.sucursalId === filtro.sucursal) .filter(v => v.fecha >= filtro.desde) .reduce((acc, v) => acc + v.total, 0); }, [ventas, filtro]); // useCallback: memoriza una función para evitar recrearla // Útil cuando la función se pasa como prop a componentes hijos const handleExportar = useCallback(() => { exportarCSV(ventasFiltradas); }, [ventasFiltradas]); // ← solo cambia si ventasFiltradas cambia return ( <> <p>Total: {ventasFiltradas}</p> <BotonExportar onExportar={handleExportar} /> // no re-renderiza innecesariamente </> ); }
React.memo(), o cuando el cálculo es genuinamente costoso (filtrar/ordenar miles de registros).
Listas y Keys
Renderizar colecciones de datos es de las tareas más comunes. React necesita una key única en cada elemento para identificar cambios eficientemente.
function ListaProductos({ productos, busqueda }) { const filtrados = productos.filter(p => p.nombre.toLowerCase().includes(busqueda.toLowerCase()) ); if (filtrados.length === 0) { return <p>Sin resultados para "{busqueda}"</p>; } return ( <ul> {filtrados.map(producto => ( <li key={producto.id}> {/* key: ID único del backend */} <TarjetaProducto nombre={producto.nombre} precio={producto.precio} /> </li> ))} </ul> ); } // ❌ NO uses el índice como key (a menos que la lista sea estática) // productos.map((p, index) => <li key={index}>) ← causa bugs en reordenamiento // ✅ Usa siempre el ID único de negocio: key={producto.id}
Formularios Controlados
En React, el estado del formulario vive en useState (componente controlado). El input siempre muestra el estado, y cada cambio actualiza el estado.
function FormularioVenta() { const [form, setForm] = useState({ producto: '', cantidad: 1, precio: '', }); const [errores, setErrores] = useState({}); // Un solo handler para todos los inputs const handleChange = (e) => { const { name, value, type } = e.target; setForm(prev => ({ ...prev, [name]: type === 'number' ? Number(value) : value, })); }; const validar = () => { const e = {}; if (!form.producto) e.producto = 'Requerido'; if (form.cantidad < 1) e.cantidad = 'Mínimo 1'; if (!form.precio || isNaN(form.precio)) e.precio = 'Precio inválido'; setErrores(e); return Object.keys(e).length === 0; }; const handleSubmit = (e) => { e.preventDefault(); if (validar()) { procesarVenta(form); setForm({ producto: '', cantidad: 1, precio: '' }); } }; return ( <form onSubmit={handleSubmit}> <input name="producto" value={form.producto} onChange={handleChange} /> {errores.producto && <span>{errores.producto}</span>} <button type="submit">Registrar Venta</button> </form> ); }
Custom Hooks
Los custom hooks son la forma más elegante de reutilizar lógica con estado. Son funciones que usan otros hooks y empiezan con use.
import { useState, useEffect } from 'react'; // Custom hook: encapsula lógica de fetch reutilizable export function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); setLoading(true); fetch(url, { signal: controller.signal }) .then(r => { if(!r.ok) throw new Error(r.statusText); return r.json(); }) .then(d => { setData(d); setError(null); }) .catch(e => { if(e.name !== 'AbortError') setError(e.message); }) .finally(() => setLoading(false)); return () => controller.abort(); // cleanup }, [url]); return { data, loading, error }; } // ────────────────────────────────────────────────────── // Uso: limpio, sin lógica de fetch en el componente function ListaSucursales() { const { data, loading, error } = useFetch('/api/sucursales'); if (loading) return <Spinner />; if (error) return <p>Error: {error}</p>; return data.map(s => <TarjetaSucursal key={s.id} sucursal={s} />); } // ─── Más custom hooks útiles ─────────────────────────── // useLocalStorage(key, initialValue) → persiste estado // useDebounce(value, delay) → input de búsqueda // useOnClickOutside(ref, handler) → cerrar dropdowns // useMediaQuery('(min-width:768px)') → responsive
Estructura de Proyecto
src/ ├── features/ ← organiza por módulo de negocio │ ├── ventas/ │ │ ├── components/ ← componentes del módulo │ │ ├── hooks/ ← custom hooks del módulo │ │ ├── services/ ← llamadas a API │ │ ├── types.ts ← TypeScript interfaces │ │ └── index.js ← exports públicos del módulo │ ├── inventario/ │ ├── reportes/ │ └── empleados/ │ ├── components/ ← componentes compartidos (UI kit) │ ├── Button.jsx │ ├── Modal.jsx │ ├── Table.jsx │ └── Spinner.jsx │ ├── hooks/ ← custom hooks globales │ ├── useFetch.js │ ├── useDebounce.js │ └── useAuth.js │ ├── context/ ← Context providers globales │ ├── AuthContext.jsx │ └── ThemeContext.jsx │ ├── services/ ← cliente HTTP, helpers de API │ └── api.js │ └── App.jsx ← routes + providers globales
ERP Carnicerías — React en Acción
Así se aplican todos los conceptos anteriores en el proyecto real del ERP, combinado con el framework MoRe (React + Moleculer).
import { useState, useEffect, useCallback } from 'react'; import { useMoleculer } from '../../../hooks/useMoleculer'; import { useBascula } from '../hooks/useBascula'; import { useOfflineSync } from '../../../hooks/useOfflineSync'; export function PantallaVenta({ sucursalId }) { const [items, setItems] = useState([]); const [cliente, setCliente] = useState(null); const [procesando,setProcesando] = useState(false); // Custom hook: conecta con Moleculer service const { call } = useMoleculer(); // Custom hook: lee peso de báscula en tiempo real const { peso, unidad } = useBascula({ port: 'COM3', baudRate: 9600 }); // Custom hook: encola ventas para sync si hay offline const { encolarVenta } = useOfflineSync(sucursalId); // Calcular total con useMemo (se recalcula solo al cambiar items) const total = useMemo( () => items.reduce((s, i) => s + i.precio * i.cantidad, 0), [items] ); const cobrar = useCallback(async () => { setProcesando(true); try { // Llama al Moleculer Sales Service const venta = await call('sales.registrarVenta', { sucursalId, items, clienteId: cliente?.id, total, }); // BullMQ encolará el CFDI automáticamente (en el service) await encolarVenta(venta); // sync si hubo offline setItems([]); // limpiar ticket } finally { setProcesando(false); } }, [items, cliente, total, sucursalId]); return ( <div className="pos-layout"> <PanelProductos onAgregar={item => setItems(p => [...p, item])} /> <Ticket items={items} total={total} pesoBascula={peso} /> <BotonCobrar onCobrar={cobrar} disabled={procesando} /> </div> ); }