Stack Tecnológico React: Estado, Testing, Arquitectura y Datos
Este artículo recorre el stack tecnológico de React: estado, testing, arquitectura frontend, datos y peticiones, herramientas de construcción, tablas, componentes, monorepos y validación. Cada apartado explica la idea central y muestra un ejemplo de código independiente.
NOTAS GENERALES
- Los ejemplos usan principalmente React, JavaScript y TypeScript.
- Cada ejemplo es independiente; no se deben pegar todos en un mismo archivo.
- Cuando un ejemplo necesita una librería, se indica en el encabezado o en el propio texto.
- Los ejemplos muestran el uso esencial. En un proyecto real también se deben considerar manejo de errores, accesibilidad, seguridad, pruebas y configuración del entorno.
- En varios apartados se utiliza TypeScript para hacer explícitos los tipos.
ESTADO
1. GESTIÓN DE ESTADO (STATE MANAGEMENT)
La gestión de estado consiste en organizar y actualizar los datos que determinan lo que una interfaz muestra y cómo responde. En React, el estado puede ser local a un componente o compartido por varios componentes. Para valores pequeños y locales suele bastar con useState; cuando muchos componentes necesitan los mismos datos, se puede usar Context, useReducer o una librería externa. Conviene distinguir el estado de interfaz (por ejemplo, si un menú está abierto) de los datos remotos que vienen de una API, porque estos últimos suelen gestionarse mejor con herramientas de consulta y caché.
Código del concepto:
import { useState } from "react";
export default function Contador() {
const [contador, setContador] = useState(0);
return (
<section>
<p>Valor: {contador}</p>
<button onClick={() => setContador((actual) => actual + 1)}>
Incrementar
</button>
</section>
);
}
2. ZUSTAND
Zustand es una librería ligera para administrar estado compartido en React. Permite crear un almacén (store) con datos y acciones, y consumir únicamente las partes necesarias desde los componentes mediante un hook. No exige envolver toda la aplicación en varios proveedores ni escribir acciones y reducers separados para cada cambio. Es útil cuando se necesita compartir estado entre componentes que no están directamente relacionados, manteniendo una API pequeña y directa.
Código del concepto:
import { create } from "zustand";
const useContadorStore = create((set) => ({
contador: 0,
incrementar: () => set((estado) => ({ contador: estado.contador + 1 })),
reiniciar: () => set({ contador: 0 })
}));
export default function Contador() {
const contador = useContadorStore((estado) => estado.contador);
const incrementar = useContadorStore((estado) => estado.incrementar);
return <button onClick={incrementar}>Contador: {contador}</button>;
}
3. REDUX
Redux es una arquitectura y librería de gestión de estado basada en un almacén central, acciones y reducers. Los componentes envían acciones para expresar qué ocurrió; un reducer recibe el estado anterior y la acción, y calcula el nuevo estado. Este flujo predecible facilita rastrear cambios, depurar y probar la lógica. Redux sigue siendo válido, aunque para aplicaciones nuevas se recomienda normalmente Redux Toolkit, que reduce mucho el código repetitivo y configura valores predeterminados útiles.
Código del concepto:
// Ejemplo conceptual del flujo clásico de Redux.
// En proyectos nuevos se recomienda Redux Toolkit.
const estadoInicial = { contador: 0 };
function contadorReducer(estado = estadoInicial, accion) {
switch (accion.type) {
case "contador/incrementar":
return { ...estado, contador: estado.contador + 1 };
case "contador/reiniciar":
return { ...estado, contador: 0 };
default:
return estado;
}
}
const accion = { type: "contador/incrementar" };
const siguienteEstado = contadorReducer(estadoInicial, accion);
console.log(siguienteEstado.contador); // 1
4. REDUX TOOLKIT
Redux Toolkit (RTK) es el conjunto oficial de herramientas recomendado para escribir aplicaciones Redux. Incluye funciones como configureStore y createSlice, incorpora middleware habitual y facilita la integración con Redux DevTools. createSlice agrupa el nombre de una parte del estado, su valor inicial y sus reducers; además permite escribir actualizaciones con una sintaxis aparentemente mutable gracias a Immer, que produce cambios inmutables internamente. RTK evita gran parte del código manual del Redux clásico.
Código del concepto:
// archivo: store.js
import { configureStore, createSlice } from "@reduxjs/toolkit";
const contadorSlice = createSlice({
name: "contador",
initialState: { valor: 0 },
reducers: {
incrementar: (state) => { state.valor += 1; },
reiniciar: (state) => { state.valor = 0; }
}
});
export const { incrementar, reiniciar } = contadorSlice.actions;
export const store = configureStore({
reducer: { contador: contadorSlice.reducer }
});
Uso desde un componente:
import { useDispatch, useSelector } from "react-redux";
import { incrementar } from "./store";
export function Contador() {
const valor = useSelector((state) => state.contador.valor);
const dispatch = useDispatch();
return <button onClick={() => dispatch(incrementar())}>Valor: {valor}</button>;
}
5. JOTAI
Jotai es una librería de estado para React que utiliza un enfoque atómico. En lugar de definir necesariamente un único objeto global grande, el estado se divide en átomos pequeños y cada componente se suscribe a los átomos que necesita. También permite crear átomos derivados cuyo valor depende de otros. Esta granularidad ayuda a mantener el estado modular y puede evitar renders innecesarios cuando solo cambia un átomo que otros componentes no utilizan.
Código del concepto:
import { atom, useAtom } from "jotai";
const contadorAtom = atom(0);
const dobleAtom = atom((get) => get(contadorAtom) * 2);
export function Contador() {
const [contador, setContador] = useAtom(contadorAtom);
return <button onClick={() => setContador((valor) => valor + 1)}>
Valor: {contador}
</button>;
}
// Un componente puede leer dobleAtom con useAtomValue(dobleAtom).
6. MOBX
MobX es una librería de gestión de estado reactiva. Permite marcar datos como observables, definir acciones que los modifican y crear componentes que reaccionan a los valores que leen. Cuando cambia un observable utilizado por un componente observer, MobX programa la actualización correspondiente. Su modelo es especialmente cómodo para aplicaciones donde los datos y sus relaciones son complejos, aunque es importante establecer límites claros para que los cambios de estado sean fáciles de seguir.
Código del concepto:
import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";
class ContadorStore {
valor = 0;
constructor() { makeAutoObservable(this); }
incrementar() { this.valor += 1; }
}
const store = new ContadorStore();
export const Contador = observer(() => (
<button onClick={() => store.incrementar()}>Valor: {store.valor}</button>
));
7. CONTEXT API
Context API es una funcionalidad integrada de React que permite compartir un valor con componentes descendientes sin tener que pasar props manualmente por cada nivel. Se crea un contexto, se coloca un Provider en una parte del árbol y los componentes consumidores leen el valor con useContext. Es apropiado para datos como el tema visual, el idioma o la sesión actual. Context no es por sí solo una solución completa para cualquier estado global; los cambios frecuentes en un contexto amplio pueden provocar renders adicionales.
Código del concepto:
import { createContext, useContext, useState } from "react";
const TemaContext = createContext(null);
function ProveedorTema({ children }) {
const [tema, setTema] = useState("claro");
return <TemaContext.Provider value={{ tema, setTema }}>
{children}
</TemaContext.Provider>;
}
function BotonTema() {
const contexto = useContext(TemaContext);
return <button onClick={() => contexto.setTema(
contexto.tema === "claro" ? "oscuro" : "claro"
)}>Tema actual: {contexto.tema}</button>;
}
export default function App() {
return <ProveedorTema><BotonTema /></ProveedorTema>;
}
8. USEREDUCER
useReducer es un hook de React para administrar estado mediante una función reducer. En lugar de actualizar el estado con varias llamadas independientes, el componente envía acciones y el reducer determina cómo debe cambiar el estado. Es útil cuando existen varias transiciones relacionadas, reglas de actualización o estados compuestos por varios campos. Puede combinarse con Context API para compartir el estado y el dispatch entre muchos componentes sin incorporar una librería externa.
Código del concepto:
import { useReducer } from "react";
const estadoInicial = { cantidad: 0 };
function reducer(estado, accion) {
switch (accion.type) {
case "incrementar": return { cantidad: estado.cantidad + 1 };
case "decrementar": return { cantidad: estado.cantidad - 1 };
case "reiniciar": return estadoInicial;
default: return estado;
}
}
export default function Contador() {
const [estado, dispatch] = useReducer(reducer, estadoInicial);
return <div>
<p>{estado.cantidad}</p>
<button onClick={() => dispatch({ type: "incrementar" })}>+</button>
<button onClick={() => dispatch({ type: "decrementar" })}>-</button>
<button onClick={() => dispatch({ type: "reiniciar" })}>Reiniciar</button>
</div>;
}
TESTING
9. PRUEBAS UNITARIAS (UNIT TESTING)
Las pruebas unitarias verifican una unidad pequeña y aislada de código, normalmente una función, método o módulo, comprobando que produce el resultado esperado para diferentes entradas. Suelen ejecutarse rápidamente y ayudan a detectar errores en reglas de negocio, cálculos y transformaciones de datos. Una buena prueba unitaria se concentra en el comportamiento de la unidad, controla sus dependencias externas cuando es necesario y no depende de una interfaz gráfica ni de una base de datos real.
Código del concepto:
// calculadora.js
export function sumar(a, b) { return a + b; }
// calculadora.test.js (ejemplo ejecutable con Vitest)
import { describe, it, expect } from "vitest";
import { sumar } from "./calculadora";
describe("sumar", () => {
it("suma dos números", () => {
expect(sumar(2, 3)).toBe(5);
});
});
10. PRUEBAS DE COMPONENTES (COMPONENT TESTING)
Las pruebas de componentes verifican un componente de interfaz dentro de un entorno de prueba, comprobando lo que se muestra y cómo responde a interacciones como clics, escritura o selección. Pueden validar estados de carga, mensajes de error, accesibilidad y cambios visibles sin tener que levantar toda la aplicación. Una prueba de componente suele usar herramientas como React Testing Library y un ejecutor como Jest o Vitest. La prueba debe enfocarse en el comportamiento que percibe el usuario, no en detalles internos de implementación.
Código del concepto:
// Saludo.jsx
export function Saludo({ nombre }) {
return <h1>Hola, {nombre}</h1>;
}
// Saludo.test.jsx
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import "@testing-library/jest-dom/vitest";
import { Saludo } from "./Saludo";
describe("Saludo", () => {
it("muestra el nombre recibido", () => {
render(<Saludo nombre="Ana" />);
expect(screen.getByRole("heading", { name: "Hola, Ana" }))
.toBeInTheDocument();
});
});
11. JEST
Jest es un framework de pruebas para JavaScript y TypeScript que proporciona un ejecutor de pruebas, aserciones, mocks, temporizadores simulados y herramientas para medir cobertura. Se utiliza para comprobar funciones, módulos y componentes, y puede integrarse con React Testing Library para probar interfaces. Jest es independiente de React: también puede probar código de Node.js u otras aplicaciones JavaScript. En proyectos con TypeScript o JSX se necesita configurar la transformación y el entorno adecuados.
Código del concepto:
// precio.js
export function calcularTotal(precio, cantidad) {
return precio * cantidad;
}
// precio.test.js
import { calcularTotal } from "./precio";
describe("calcularTotal", () => {
test("multiplica precio por cantidad", () => {
expect(calcularTotal(25, 4)).toBe(100);
});
});
12. VITEST
Vitest es un ejecutor de pruebas pensado para integrarse con proyectos que utilizan Vite, aunque también puede funcionar en otros entornos compatibles. Ofrece una API familiar para quienes conocen Jest, con funciones como describe, it, expect y vi para mocks. Su integración con la configuración de Vite permite reutilizar alias y transformaciones del proyecto, reduciendo la duplicación de configuración. Es una opción frecuente para probar aplicaciones modernas creadas con Vite.
Código del concepto:
// multiplicar.js
export const multiplicar = (a, b) => a * b;
// multiplicar.test.js
import { describe, it, expect } from "vitest";
import { multiplicar } from "./multiplicar";
describe("multiplicar", () => {
it("multiplica dos valores", () => {
expect(multiplicar(6, 7)).toBe(42);
});
});
13. TESTING LIBRARY
Testing Library es una familia de utilidades para probar interfaces desde la perspectiva del usuario. En React, React Testing Library permite renderizar componentes y consultar elementos por su rol, etiqueta, texto u otras propiedades accesibles. También facilita simular interacciones, normalmente mediante user-event. Su filosofía recomienda evitar pruebas que dependan de nombres de funciones internas, estado privado o estructura exacta del DOM, porque esos detalles pueden cambiar sin que la experiencia del usuario cambie.
Código del concepto:
import React, { useState } from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect } from "vitest";
import "@testing-library/jest-dom/vitest";
function BotonContador() {
const [valor, setValor] = useState(0);
return <button onClick={() => setValor(valor + 1)}>
Pulsado {valor} veces
</button>;
}
describe("BotonContador", () => {
it("actualiza el texto al pulsar", async () => {
const usuario = userEvent.setup();
render(<BotonContador />);
await usuario.click(screen.getByRole("button"));
expect(screen.getByRole("button")).toHaveTextContent("Pulsado 1 veces");
});
});
ARQUITECTURA FRONTEND
14. MICROFRONTENDS
Los microfrontends son una estrategia para dividir una aplicación frontend grande en partes más pequeñas que pueden desarrollarse, probarse y desplegarse con cierto grado de independencia. Cada equipo puede ser responsable de una funcionalidad o dominio, por ejemplo catálogo, pagos o administración. Las partes se integran en una experiencia común mediante mecanismos de composición, rutas o carga remota. Esta arquitectura puede facilitar la autonomía de equipos, pero también introduce complejidad de integración, observabilidad, estilos, navegación, seguridad y compatibilidad entre versiones.
Código del concepto:
Aplicación principal
|
+-- Microfrontend de catálogo
| +-- Lista de productos
| +-- Detalle de producto
|
+-- Microfrontend de pagos
| +-- Carrito
| +-- Confirmación
|
+-- Microfrontend de cuenta
+-- Inicio de sesión
+-- Perfil
Cada microfrontend puede tener su propio pipeline de pruebas y despliegue.
La aplicación principal define cómo se integran.
15. MODULE FEDERATION
Module Federation es una capacidad de Webpack 5 que permite que distintas compilaciones JavaScript expongan y consuman módulos entre sí durante la ejecución. Una aplicación puede publicar un módulo mediante exposes y otra cargarlo mediante remotes, sin necesitar empaquetar todo el código remoto dentro de la compilación del consumidor. Se utiliza con frecuencia para implementar microfrontends, aunque también sirve para compartir módulos entre aplicaciones. Requiere acordar contratos, versiones, disponibilidad de los remotos y estrategia ante fallos de carga.
Código del concepto:
// webpack.config.js de una aplicación remota
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
// ...resto de la configuración de Webpack
plugins: [
new ModuleFederationPlugin({
name: "catalogo",
filename: "remoteEntry.js",
exposes: { "./Producto": "./src/Producto" },
shared: ["react", "react-dom"]
})
]
};
La aplicación host configura el remote y puede importar el módulo expuesto.
16. HOST / REMOTES
En Module Federation, el host es la aplicación que consume módulos federados, mientras que un remote es una compilación que publica módulos para que otros los utilicen. El host conoce la ubicación del punto de entrada remoto, normalmente un archivo como remoteEntry.js, y después importa el módulo expuesto. Esta separación permite que una aplicación central integre funcionalidades desarrolladas por otros equipos. Host y remote deben acordar nombres, rutas, contratos de componentes y dependencias compatibles.
Código del concepto:
// Configuración simplificada del HOST
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "host",
remotes: {
catalogo: "catalogo@http://localhost:3001/remoteEntry.js"
}
})
]
};
// En un componente del host, con soporte de importación remota configurado:
// const Producto = React.lazy(() => import("catalogo/Producto"));
17. GESTIÓN DE DEPENDENCIAS COMPARTIDAS
La gestión de dependencias compartidas en microfrontends consiste en decidir qué paquetes deben reutilizarse entre host y remotes y qué versiones son compatibles. Compartir librerías como React puede evitar cargar varias copias, reducir el tamaño descargado y prevenir problemas derivados de tener dos instancias de una misma librería. Sin embargo, declarar una dependencia como singleton exige cuidar la compatibilidad de versiones y el orden de carga. La decisión debe basarse en el tamaño, el comportamiento de la librería y la autonomía que necesita cada microfrontend.
Código del concepto:
// Fragmento de ModuleFederationPlugin
new ModuleFederationPlugin({
name: "host",
shared: {
react: {
singleton: true,
requiredVersion: "^18.2.0"
},
"react-dom": {
singleton: true,
requiredVersion: "^18.2.0"
}
}
});
Las versiones requeridas deben coincidir con las versiones reales instaladas y con la estrategia de compatibilidad del proyecto.
GESTIÓN DE DATOS Y PETICIONES
18. TANSTACK QUERY
TanStack Query es una librería para gestionar datos remotos o server state en aplicaciones frontend. Proporciona caché, consultas, revalidación, estados de carga y error, invalidación y sincronización de datos con un servidor. En lugar de guardar manualmente cada respuesta de API en estado local, se define una query con una clave y una función que obtiene los datos. La librería administra su ciclo de vida y permite actualizar o invalidar la caché cuando una operación modifica información.
Código del concepto:
import {
QueryClient,
QueryClientProvider,
useQuery
} from "@tanstack/react-query";
const queryClient = new QueryClient();
function Usuarios() {
const { data, isPending, isError } = useQuery({
queryKey: ["usuarios"],
queryFn: async () => {
const respuesta = await fetch("/api/usuarios");
if (!respuesta.ok) throw new Error("No se pudieron cargar usuarios");
return respuesta.json();
}
});
if (isPending) return <p>Cargando...</p>;
if (isError) return <p>Error al consultar usuarios</p>;
return <ul>{data.map((u) => <li key={u.id}>{u.nombre}</li>)}</ul>;
}
// En el punto de entrada:
// <QueryClientProvider client={queryClient}><App /></QueryClientProvider>
19. REACT QUERY
React Query fue el nombre anterior de TanStack Query. El nombre del proyecto cambió al ampliar su enfoque más allá de React, pero TanStack Query sigue ofreciendo integración específica para React mediante su paquete correspondiente. Por eso, en documentación y proyectos antiguos puede aparecer el nombre React Query, mientras que en proyectos actuales es habitual encontrar @tanstack/react-query. El concepto es el mismo: administrar datos remotos, caché, carga, errores, revalidación y mutaciones.
Código del concepto:
// API actual de TanStack Query para React.
// "React Query" es el nombre histórico del proyecto.
import { useQuery } from "@tanstack/react-query";
function Perfil({ usuarioId }) {
const consulta = useQuery({
queryKey: ["perfil", usuarioId],
queryFn: async () => {
const respuesta = await fetch(`/api/usuarios/${usuarioId}`);
if (!respuesta.ok) throw new Error("Error HTTP");
return respuesta.json();
},
enabled: Boolean(usuarioId)
});
if (consulta.isPending) return <p>Cargando perfil...</p>;
if (consulta.isError) return <p>No se pudo cargar el perfil</p>;
return <h2>{consulta.data.nombre}</h2>;
}
20. SWR
SWR es una librería de React para obtener y mantener datos remotos utilizando la estrategia stale-while-revalidate: puede mostrar primero datos disponibles en caché y después solicitar una versión actualizada al servidor. Ofrece deduplicación de peticiones, revalidación al recuperar el foco de la ventana y manejo de estados de carga y error. Es útil para interfaces que necesitan mostrar datos recientes sin implementar manualmente toda la lógica de caché y sincronización.
Código del concepto:
import useSWR from "swr";
const obtenerJSON = async (url) => {
const respuesta = await fetch(url);
if (!respuesta.ok) throw new Error("Error al consultar API");
return respuesta.json();
};
export function Usuarios() {
const { data, error, isLoading } = useSWR("/api/usuarios", obtenerJSON);
if (isLoading) return <p>Cargando...</p>;
if (error) return <p>Error al cargar los usuarios</p>;
return <ul>{data.map((usuario) =>
<li key={usuario.id}>{usuario.nombre}</li>
)}</ul>;
}
21. RTK QUERY
RTK Query es una herramienta de obtención y caché de datos incluida en Redux Toolkit. Se definen endpoints mediante createApi y se genera automáticamente un conjunto de hooks para consultar o modificar datos. La caché se organiza mediante claves y etiquetas, lo que permite invalidar consultas relacionadas después de una mutación. Es especialmente conveniente cuando una aplicación ya utiliza Redux Toolkit y quiere integrar la gestión de datos del servidor con su store.
Código del concepto:
// archivo: api.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
endpoints: (builder) => ({
obtenerUsuarios: builder.query({ query: () => "/usuarios" })
})
});
export const { useObtenerUsuariosQuery } = api;
// El store debe incorporar api.reducer y api.middleware.
// En un componente:
function Usuarios() {
const { data, isLoading, error } = useObtenerUsuariosQuery();
if (isLoading) return <p>Cargando...</p>;
if (error) return <p>Error</p>;
return <ul>{data.map((u) => <li key={u.id}>{u.nombre}</li>)}</ul>;
}
22. APOLLO CLIENT
Apollo Client es una librería para trabajar con APIs GraphQL desde el frontend. Permite escribir consultas y mutaciones GraphQL, administrar resultados en una caché normalizada y actualizar la interfaz cuando cambian los datos. Se integra con React mediante ApolloProvider y hooks como useQuery. Es especialmente útil cuando el backend expone un esquema GraphQL y el frontend necesita solicitar exactamente los campos que requiere, aunque implica conocer el modelo de consultas y tipos de GraphQL.
Código del concepto:
import {
ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery
} from "@apollo/client";
const cliente = new ApolloClient({
uri: "/graphql",
cache: new InMemoryCache()
});
const CONSULTA_USUARIOS = gql`
query Usuarios {
usuarios { id nombre }
}
`;
function Usuarios() {
const { loading, error, data } = useQuery(CONSULTA_USUARIOS);
if (loading) return <p>Cargando...</p>;
if (error) return <p>Error al consultar GraphQL</p>;
return <ul>{data.usuarios.map((u) =>
<li key={u.id}>{u.nombre}</li>
)}</ul>;
}
// En la raíz: <ApolloProvider client={cliente}><App /></ApolloProvider>
HERRAMIENTAS DE CONSTRUCCIÓN
23. WEBPACK
Webpack es un empaquetador de módulos (module bundler) que analiza las dependencias de una aplicación y construye uno o varios archivos de salida para el navegador u otro entorno. Parte de un punto de entrada, recorre los módulos importados y aplica loaders y plugins para transformar recursos, optimizar código y generar bundles. Puede procesar JavaScript, TypeScript, CSS, imágenes y otros recursos mediante las herramientas adecuadas. Se utiliza para controlar cómo se compila y entrega una aplicación frontend.
Código del concepto:
// webpack.config.js
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
clean: true
}
};
24. CONFIGURACIÓN Y MANTENIMIENTO DE WEBPACK
Configurar y mantener Webpack implica definir entradas, salidas, loaders, plugins, resolución de módulos, variables de entorno y diferencias entre desarrollo y producción. Una configuración mantenible separa lo común de lo específico de cada entorno, evita loaders innecesarios y mantiene las versiones de dependencias actualizadas. También debe documentar decisiones relevantes y permitir que el equipo entienda cómo se transforman los recursos. Una configuración mínima puede ampliarse gradualmente cuando el proyecto necesita TypeScript, CSS, imágenes, análisis de bundles o división de código.
Código del concepto:
// webpack.config.js: reglas y plugin para CSS.
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: process.env.NODE_ENV === "production" ? "production" : "development",
entry: "./src/index.js",
output: {
filename: "[name].[contenthash].js",
path: path.resolve(__dirname, "dist"),
clean: true
},
module: {
rules: [{ test: /\.css$/i, use: ["style-loader", "css-loader"] }]
},
plugins: [new HtmlWebpackPlugin({ template: "./src/index.html" })]
};
Para una aplicación real se deben instalar los loaders y plugins indicados y configurar los loaders de JavaScript o TypeScript según el compilador elegido.
25. RESOLUCIÓN DE PROBLEMAS DE COMPILACIÓN
Resolver problemas de compilación consiste en identificar por qué el proceso que transforma el código fuente en archivos ejecutables falla o produce resultados incorrectos. El método habitual es reproducir el error, leer el primer mensaje relevante del log, localizar el archivo y módulo involucrados, verificar versiones y revisar loaders, plugins, aliases y variables de entorno. También conviene limpiar artefactos generados, instalar dependencias de forma reproducible y aislar el cambio que introdujo el problema, en vez de modificar la configuración al azar.
Código del concepto:
{
"scripts": {
"build": "webpack --mode production",
"build:debug": "webpack --mode production --stats-error-details",
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\""
}
}
Comandos de diagnóstico:
npm ls webpack webpack-cli
npm run clean
npm run build:debug
TABLAS Y VISUALIZACIÓN DE DATOS
26. AG GRID
AG Grid es una librería de tablas de datos para aplicaciones web que ofrece funciones avanzadas como ordenamiento, filtros, selección, edición, agrupación y paginación. Está diseñada para mostrar y manipular conjuntos de datos tabulares, desde listas sencillas hasta escenarios empresariales. Dispone de una edición Community y capacidades adicionales en Enterprise. La configuración de columnas determina cómo se presenta y edita cada campo, mientras que los datos se proporcionan como filas.
Código del concepto:
import { useMemo } from "react";
import { AgGridReact } from "ag-grid-react";
export default function TablaUsuarios() {
const columnas = useMemo(() => [
{ field: "nombre" },
{ field: "correo" },
{ field: "activo" }
], []);
const filas = [
{ id: 1, nombre: "Ana", correo: "ana@ejemplo.com", activo: true },
{ id: 2, nombre: "Luis", correo: "luis@ejemplo.com", activo: false }
];
return <div style={{ height: 300 }}>
<AgGridReact
rowData={filas}
columnDefs={columnas}
getRowId={(params) => String(params.data.id)}
/>
</div>;
}
La configuración de tema y registro de módulos puede variar según la versión de AG Grid instalada.
27. TABLAS DE DATOS COMPLEJAS
Una tabla de datos compleja es una interfaz que permite consultar y operar sobre muchas filas y columnas, a menudo con relaciones, estados, selección múltiple, agrupaciones, resúmenes y acciones por fila. Además de mostrar valores, debe comunicar claramente qué se puede editar, cómo se ordenan los datos y qué ocurre al realizar una operación. En lugar de construir todas esas capacidades desde cero, suele utilizarse una librería de tablas y definir columnas, identificadores únicos y funciones de interacción de forma explícita.
Código del concepto:
const columnas = [
{ field: "id", headerName: "ID" },
{ field: "cliente", headerName: "Cliente" },
{ field: "total", headerName: "Total" },
{
field: "estado",
headerName: "Estado",
cellRenderer: (params) => params.value.toUpperCase()
}
];
const filas = [
{ id: 101, cliente: "Comercial Norte", total: 1250, estado: "pendiente" },
{ id: 102, cliente: "Distribuidora Sur", total: 980, estado: "pagado" }
];
// Estas definiciones pueden pasarse como columnDefs y rowData de AG Grid.
28. FILTROS
Los filtros permiten reducir las filas visibles de una tabla según criterios elegidos por el usuario, como texto, rango numérico, fechas o valores específicos. Un filtro puede aplicarse en el cliente cuando todos los datos están cargados, o en el servidor cuando el volumen es grande y la consulta debe ejecutarse remotamente. Para una experiencia coherente, la interfaz debe mostrar los criterios activos, permitir limpiarlos y comunicar cuándo no existen resultados.
Código del concepto:
// Ejemplo de configuración de filtros en columnas de AG Grid.
const columnas = [
{ field: "nombre", filter: "agTextColumnFilter" },
{ field: "edad", filter: "agNumberColumnFilter" },
{ field: "activo", filter: true }
];
// En la tabla: <AgGridReact rowData={filas} columnDefs={columnas} />
29. EDICIÓN DE DATOS
La edición de datos en una tabla permite modificar valores directamente desde las celdas o mediante un formulario asociado a una fila. Es necesario definir qué columnas son editables, validar los nuevos valores y decidir cuándo se guardan: inmediatamente, al confirmar una fila o mediante un botón de guardar. En aplicaciones conectadas a un servidor, los cambios deben persistirse mediante una petición y la interfaz debe manejar errores o conflictos sin perder silenciosamente la información.
Código del concepto:
// Ejemplo de edición de celdas con AG Grid.
const columnas = [
{ field: "nombre", editable: true },
{ field: "precio", editable: true },
{ field: "categoria", editable: false }
];
function alCambiarCelda(evento) {
console.log("Fila modificada:", evento.data);
// En una aplicación real, aquí se puede validar y guardar en la API.
}
// En la tabla:
// <AgGridReact rowData={filas} columnDefs={columnas}
// onCellValueChanged={alCambiarCelda} />
30. VIRTUALIZACIÓN
La virtualización consiste en renderizar únicamente las filas o elementos que están visibles en la zona de desplazamiento, más un pequeño margen, en lugar de crear nodos para todos los registros al mismo tiempo. Esto reduce el trabajo del navegador y el consumo de memoria cuando se manejan miles de filas. Muchas librerías de tablas implementan virtualización internamente. Debe probarse con alturas, desplazamiento, selección y accesibilidad, porque una fila que no está visible puede no existir temporalmente en el DOM.
Código del concepto:
// AG Grid incorpora virtualización de filas.
// El contenedor debe tener una altura definida para que exista scroll.
function TablaGrande({ filas, columnas }) {
return <div style={{ height: 500, width: "100%" }}>
<AgGridReact
rowData={filas}
columnDefs={columnas}
rowBuffer={10}
/>
</div>;
}
31. OPTIMIZACIÓN DE RENDIMIENTO
La optimización del rendimiento de una tabla busca reducir renders innecesarios, cálculos repetidos y trabajo de manipulación del DOM. Algunas medidas habituales son mantener estables las definiciones de columnas, usar identificadores únicos de filas, evitar renderizadores costosos, aplicar virtualización y paginar o filtrar en el servidor cuando el conjunto de datos es grande. La optimización debe basarse en mediciones: primero se identifica el cuello de botella y después se aplica el cambio que realmente lo reduce.
Código del concepto:
import { useMemo, useCallback } from "react";
import { AgGridReact } from "ag-grid-react";
function Tabla({ filas, guardar }) {
const columnas = useMemo(() => [
{ field: "nombre" },
{ field: "estado" }
], []);
const alCambiar = useCallback((evento) => {
guardar(evento.data);
}, [guardar]);
const obtenerId = useCallback(
(params) => String(params.data.id),
[]
);
return <AgGridReact
rowData={filas}
columnDefs={columnas}
getRowId={obtenerId}
onCellValueChanged={alCambiar}
/>;
}
32. PAGINACIÓN
La paginación divide un conjunto de resultados en páginas para que el usuario consulte una parte manejable de los datos en cada momento. Puede realizarse en el cliente cuando todos los registros están disponibles, o en el servidor cuando se consulta un conjunto grande. La paginación del servidor debe enviar el número de página o un cursor y el tamaño solicitado, y recibir tanto los resultados como la información necesaria para saber si existen más páginas.
Código del concepto:
// Activar paginación en AG Grid.
function TablaPaginada({ filas, columnas }) {
return <div style={{ height: 450 }}>
<AgGridReact
rowData={filas}
columnDefs={columnas}
pagination={true}
paginationPageSize={20}
paginationPageSizeSelector={[10, 20, 50, 100]}
/>
</div>;
}
DISEÑO Y ARQUITECTURA DE COMPONENTES
33. DESIGN SYSTEMS
Un Design System es un conjunto organizado de principios, tokens, componentes, patrones y reglas que permiten construir interfaces consistentes. Puede incluir colores, tipografías, espaciados, escalas, estados, iconografía, pautas de accesibilidad y reglas de uso para formularios, navegación y otros elementos. Su propósito no es solo que la interfaz se vea uniforme, sino que equipos distintos tomen decisiones compatibles y puedan mantener el producto a lo largo del tiempo.
Código del concepto:
/* tokens de diseño: valores reutilizables */
:root {
--color-primario: #155eef;
--color-texto: #182230;
--color-fondo: #ffffff;
--espaciado-base: 8px;
--radio-control: 6px;
}
.boton-primario {
color: white;
background: var(--color-primario);
padding: calc(var(--espaciado-base) * 1.5);
border: 0;
border-radius: var(--radio-control);
}
34. COMPONENT LIBRARIES
Una Component Library es una colección de componentes de interfaz reutilizables, como botones, campos de texto, menús, modales y tarjetas. Cada componente debe tener una API clara, estados definidos, estilos coherentes y documentación sobre sus propiedades y uso. Una librería puede ser interna de una empresa o pública, y puede publicarse como paquete para instalarse en varias aplicaciones. Es importante probar los componentes y mantener compatibilidad para no romper a los proyectos que dependen de ellos.
Código del concepto:
// Button.tsx: componente reutilizable y tipado.
type ButtonProps = {
children: React.ReactNode;
disabled?: boolean;
onClick?: () => void;
};
export function Button({
children,
disabled = false,
onClick
}: ButtonProps) {
return <button disabled={disabled} onClick={onClick}>{children}</button>;
}
// Uso: <Button onClick={guardar}>Guardar</Button>
35. LIBRERÍAS DE COMPONENTES COMPARTIDAS
Una librería de componentes compartida es un paquete que se desarrolla una vez y se consume desde varias aplicaciones o microfrontends. Centraliza componentes, tokens, estilos y utilidades para evitar copias divergentes y facilitar correcciones globales. Para que funcione bien, debe definir reglas de versionado, compatibilidad, publicación y cambios de API. También conviene evitar que la librería concentre lógica específica de una sola aplicación, porque eso haría que los demás consumidores dependan de decisiones que no les corresponden.
Código del concepto:
estructura del monorepo:
apps/
tienda/
administracion/
packages/
ui/
src/
Button.tsx
Input.tsx
package.json
package.json de packages/ui:
{
"name": "@empresa/ui",
"version": "1.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts"
}
exportación en packages/ui/src/index.ts:
export { Button } from "./Button";
export { Input } from "./Input";
consumo desde una aplicación:
import { Button } from "@empresa/ui";
MONOREPOS
36. TURBOREPO
Turborepo es un sistema de construcción para monorepos JavaScript y TypeScript. Permite definir tareas como build, test y lint para múltiples paquetes, entender sus dependencias, ejecutarlas en el orden correcto y reutilizar resultados mediante caché. Esto evita repetir trabajo cuando los archivos de una tarea no han cambiado. Es especialmente útil cuando un repositorio contiene varias aplicaciones y paquetes compartidos que forman parte de un mismo producto.
Código del concepto:
// turbo.json
{
"$schema": "https://turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", "build/**"]
},
"test": { "dependsOn": ["build"], "outputs": [] },
"lint": { "outputs": [] }
}
}
El operador ^build indica que primero se construyen las dependencias del paquete.
37. YARN WORKSPACES
Yarn Workspaces es una función de Yarn que permite administrar varios paquetes dentro de un mismo repositorio desde un proyecto raíz. Los paquetes pueden declarar dependencias entre sí y Yarn se encarga de enlazarlos localmente durante la instalación. Esto facilita desarrollar una aplicación junto con una librería compartida sin publicar cada cambio para probarlo. El archivo package.json de la raíz declara los directorios que contienen los workspaces.
Código del concepto:
// package.json en la raíz
{
"private": true,
"workspaces": ["apps/*", "packages/*"]
}
Ejemplo de estructura:
proyecto/
package.json
apps/
web/
package.json
packages/
ui/
package.json
Desde un workspace, la aplicación puede declarar @empresa/ui como dependencia y utilizar el paquete local según la configuración de Yarn.
38. NX
Nx es un sistema de herramientas para construir y administrar monorepos, con soporte para múltiples frameworks y lenguajes. Ayuda a organizar aplicaciones y librerías, definir tareas, analizar el grafo de dependencias, ejecutar solo lo afectado por un cambio y aprovechar caché local o distribuida. Además de la ejecución de tareas, ofrece generadores y convenciones para mantener una estructura consistente. Es útil en repositorios grandes donde se necesita controlar las relaciones entre proyectos.
Código del concepto:
// project.json de una aplicación
{
"name": "tienda",
"sourceRoot": "apps/tienda/src",
"projectType": "application",
"targets": {
"build": {
"executor": "nx:run-commands",
"options": { "command": "vite build" }
},
"test": {
"executor": "nx:run-commands",
"options": { "command": "vitest run" }
}
}
}
La configuración exacta de targets depende de los plugins y versiones de Nx instalados.
39. LERNA
Lerna es una herramienta para administrar proyectos JavaScript con varios paquetes dentro de un repositorio. Ayuda a ejecutar comandos en paquetes, gestionar versiones y publicar paquetes relacionados. En proyectos actuales puede utilizarse junto con herramientas de monorepo como Nx, y su configuración debe adaptarse al gestor de paquetes elegido. Es especialmente relevante en repositorios que publican varias librerías que comparten ciclo de desarrollo y necesitan coordinar sus versiones.
Código del concepto:
// lerna.json
{
"$schema": "https://json.schemastore.org/lerna.json",
"version": "independent",
"packages": ["packages/*"]
}
Comandos de ejemplo:
npx lerna run build
npx lerna run test --scope=@empresa/ui
40. PNPM WORKSPACES
pnpm Workspaces permite administrar múltiples paquetes en un mismo repositorio usando pnpm como gestor de dependencias. Los paquetes se declaran en pnpm-workspace.yaml y pueden referenciarse entre sí mediante el protocolo workspace:. pnpm utiliza un almacén de contenido compartido y enlaces para instalar dependencias eficientemente, manteniendo una estructura que ayuda a detectar dependencias no declaradas. Es una opción frecuente para monorepos con varias aplicaciones y paquetes internos.
Código del concepto:
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
Dependencia local en el package.json de una aplicación:
{
"dependencies": {
"@empresa/ui": "workspace:*"
}
}
Instalación desde la raíz:
pnpm install
VALIDACIÓN DE DATOS
41. ZOD
Zod es una librería de validación y definición de esquemas para JavaScript y TypeScript. Permite describir la forma y las reglas que deben cumplir los datos, por ejemplo tipos, campos obligatorios, valores mínimos y formatos. A partir de un esquema puede inferirse un tipo de TypeScript, lo que reduce la duplicación entre validación y tipado. Es útil para validar formularios, parámetros, respuestas de API y datos que llegan desde fuentes externas, que no deben considerarse confiables solo porque el código tenga tipos.
Código del concepto:
import { z } from "zod";
const UsuarioSchema = z.object({
nombre: z.string().min(2),
correo: z.email(),
edad: z.number().int().min(18)
});
type Usuario = z.infer<typeof UsuarioSchema>;
const resultado = UsuarioSchema.safeParse({
nombre: "Ana",
correo: "ana@ejemplo.com",
edad: 25
});
if (resultado.success) {
const usuario: Usuario = resultado.data;
console.log(usuario.nombre);
} else {
console.error(resultado.error.issues);
}
Nota: z.email() corresponde a la API moderna de Zod 4. En proyectos que usan Zod 3, suele escribirse z.string().email().
42. YUP
Yup es una librería para construir esquemas de validación de datos mediante una API encadenable. Permite especificar tipos, campos requeridos, longitudes, rangos y reglas personalizadas. Se utiliza con frecuencia para validar formularios y puede integrarse con bibliotecas como Formik. A diferencia de una simple comprobación visual, Yup valida los datos de forma programática y puede devolver errores asociados a los campos. Es importante decidir explícitamente cuándo convertir tipos y cómo tratar valores vacíos.
Código del concepto:
import * as yup from "yup";
const usuarioSchema = yup.object({
nombre: yup.string().min(2).required("El nombre es obligatorio"),
correo: yup.string().email("Correo no válido").required(),
edad: yup.number().min(18).required()
});
async function validarUsuario(datos: unknown) {
try {
const usuarioValidado = await usuarioSchema.validate(datos, {
abortEarly: false
});
console.log(usuarioValidado);
} catch (error) {
console.error(error);
}
}
43. VALIBOT
Valibot es una librería modular de validación de datos para JavaScript y TypeScript. Permite construir esquemas mediante funciones pequeñas que se combinan, con una arquitectura orientada a reducir el código que se incluye cuando solo se importan las operaciones necesarias. Puede validar objetos, cadenas, números, arrays y reglas personalizadas. Es una alternativa a otras librerías de esquemas cuando se busca una API funcional y modular, manteniendo tipos inferidos desde los esquemas.
Código del concepto:
import * as v from "valibot";
const UsuarioSchema = v.object({
nombre: v.pipe(v.string(), v.minLength(2)),
correo: v.pipe(v.string(), v.email()),
edad: v.pipe(v.number(), v.minValue(18))
});
const resultado = v.safeParse(UsuarioSchema, {
nombre: "Ana",
correo: "ana@ejemplo.com",
edad: 25
});
if (resultado.success) {
console.log(resultado.output);
} else {
console.error(resultado.issues);
}
44. ESQUEMAS DE VALIDACIÓN
Un esquema de validación es una descripción formal de la estructura y las reglas que debe cumplir un conjunto de datos. Puede indicar qué campos son obligatorios, qué tipos aceptan, qué límites tienen y cómo se relacionan entre sí. Los esquemas permiten comprobar datos de formularios, respuestas de servicios o archivos antes de utilizarlos. Centralizar esas reglas reduce inconsistencias y facilita reutilizar la misma validación en distintos puntos, aunque la validación crítica también debe repetirse en el backend: la validación del frontend no sustituye la seguridad del servidor.
Código del concepto:
// Ejemplo sin librería: validación explícita con TypeScript.
type Registro = {
nombre: string;
correo: string;
edad: number;
};
function validarRegistro(valor: unknown): valor is Registro {
if (typeof valor !== "object" || valor === null) return false;
const dato = valor as Record<string, unknown>;
return (
typeof dato.nombre === "string" &&
dato.nombre.trim().length >= 2 &&
typeof dato.correo === "string" &&
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(dato.correo) &&
typeof dato.edad === "number" &&
Number.isInteger(dato.edad) &&
dato.edad >= 18
);
}
const entrada: unknown = {
nombre: "Ana",
correo: "ana@ejemplo.com",
edad: 25
};
if (validarRegistro(entrada)) {
// Dentro de este bloque TypeScript reconoce el tipo Registro.
console.log(entrada.nombre);
}
RESUMEN DE RELACIONES IMPORTANTES
- State Management es el problema general; Zustand, Redux, Redux Toolkit, Jotai y MobX son soluciones de gestión de estado.
- Context API y useReducer son herramientas integradas de React. Pueden combinarse para compartir estado sin una librería externa.
- Redux es el patrón y ecosistema; Redux Toolkit es la forma recomendada de escribir Redux moderno.
- Unit Testing prueba unidades pequeñas; Component Testing prueba la interfaz y su comportamiento.
- Jest y Vitest ejecutan pruebas. Testing Library ayuda a interactuar con componentes desde la perspectiva del usuario.
- Microfrontends es una arquitectura; Module Federation es una de las tecnologías que puede facilitar su integración.
- Host consume módulos federados; Remote los expone. Las dependencias compartidas deben tener versiones compatibles.
- TanStack Query es el nombre actual del proyecto antes conocido como React Query.
- SWR, TanStack Query, RTK Query y Apollo Client resuelven necesidades relacionadas con datos remotos, pero no usan el mismo modelo ni están pensadas exactamente para los mismos escenarios.
- Webpack construye bundles; loaders transforman recursos y plugins amplían el proceso de compilación.
- AG Grid ofrece capacidades de tabla; filtros, edición, virtualización, paginación y optimización son funcionalidades o prácticas alrededor de la tabla.
- Design System define reglas y patrones; Component Library implementa componentes reutilizables que pueden seguir ese sistema.
- Turborepo, Nx, Lerna, Yarn Workspaces y pnpm Workspaces son herramientas relacionadas con la organización y automatización de monorepos, pero no son equivalentes entre sí.
- Zod, Yup y Valibot permiten definir esquemas y validar datos. La validación del frontend nunca reemplaza la validación del backend.
© Arturo Juárez Monroy