⚡ CultivaMetrics — Guía de Modernización JS
JavaScript ES6+ Patterns
Referencia para Devs
Guía práctica para migrar el codebase de CultivaMetrics de callbacks y ES5 a patrones modernos. Cada sección muestra el código legacy y su equivalente moderno con ejemplos del dominio de analítica de marketing.
8+
Categorías
25+
Patrones
15
Best Practices
ES2022
Versión target
Variables // const · let · var
Declaración correcta de variables
Legacy
var campanas = 0; var clienteActivo = true; var nombre = "CultivaMetrics"; // Problema: var tiene scope // de función, no de bloque for (var i = 0; i < 3; i++) { // i "escapa" al scope } console.log(i); // 3 (leak!)
Moderno
const campanias = 0; // inmutable const clienteActivo = true; const nombre = "CultivaMetrics"; // let solo cuando reasignas let totalClicks = 0; for (let i = 0; i < 3; i++) { // i confinada al bloque } // i no existe aquí ✓
Template literals
Legacy
const cliente = "Acme Corp"; const ctr = 3.7; const msg = "Cliente: " + cliente + " | CTR: " + ctr + "%"; // Multi-línea: imposible limpio const html = "<div>" + "<h2>" + cliente + "</h2>" + "</div>";
Moderno
const cliente = "Acme Corp"; const ctr = 3.7; const msg = `Cliente: ${cliente} | CTR: ${ctr}%`; // Multi-línea y expresiones const html = ` <div> <h2>${cliente}</h2> <p>CTR: ${(ctr * 1.1).toFixed(2)}%</p> </div> `;
Arrow functions — léxico de
thisLegacy — problema con this
var CampaniaTracker = function() { this.clicks = 0; }; CampaniaTracker.prototype.iniciar = function() { setInterval(function() { this.clicks++; // ❌ this = window/undefined console.log(this.clicks); }, 1000); };
Moderno — arrow preserva this
class CampaniaTracker { clicks = 0; // Arrow field = this siempre correcto registrarClick = () => { this.clicks++; }; iniciar() { setInterval(() => { this.clicks++; // ✓ this = instancia console.log(this.clicks); }, 1000); } }
Desestructuración // objects · arrays · params
Desestructuración de objetos
Legacy
const campania = { id: "cm-2024-01", nombre: "Black Friday", presupuesto: 5000, metricas: { ctr: 4.2, roas: 8.1 } }; var id = campania.id; var nombre = campania.nombre; var ctr = campania.metricas.ctr; var roas = campania.metricas.roas;
Moderno
const campania = { id: "cm-2024-01", nombre: "Black Friday", presupuesto: 5000, metricas: { ctr: 4.2, roas: 8.1 } }; const { id, nombre, metricas: { ctr, roas } } = campania; // Alias + default const { presupuesto: budget = 1000 } = campania;
Parámetros de función
Legacy
function calcularROAS(campania) { var ingresos = campania.ingresos; var gasto = campania.gasto; var nombre = campania.nombre; return { nombre: nombre, roas: ingresos / gasto }; }
Moderno
const calcularROAS = ({ nombre, ingresos, gasto = 1 // default evita /0 }) => ({ nombre, roas: ingresos / gasto }); // Shorthand: nombre: nombre → nombre
Desestructuración de arrays
Legacy
var top3 = ["Google Ads", "Meta", "TikTok"]; var primero = top3[0]; var segundo = top3[1]; var resto = top3.slice(2); // Intercambio verboso var tmp = primero; primero = segundo; segundo = tmp;
Moderno
const [primero, segundo, ...resto] = [ "Google Ads", "Meta", "TikTok", "LinkedIn" ]; // Intercambio sin tmp variable [primero, segundo] = [segundo, primero]; // Skip elementos const [,, tercero] = top3;
Retorno múltiple de funciones
// Función que retorna múltiples métricas const analizarCampania = (datos) => { const ctr = datos.clicks / datos.impresiones * 100; const roas = datos.ingresos / datos.gasto; const cpa = datos.gasto / datos.conversiones; return { ctr, roas, cpa }; // shorthand }; // Consumir con desestructuración directa const { ctr, roas } = analizarCampania(datosCampania); console.log(`CTR: ${ctr.toFixed(2)}% | ROAS: ${roas.toFixed(1)}x`);
Spread / Rest // immutabilidad · composición
Actualización inmutable de estado
Legacy — mutación directa
var campania = { nombre: "Summer Sale", activa: false, metricas: { clicks: 1200 } }; // ❌ Mutación directa — bugs en React/stores campania.activa = true; campania.metricas.clicks = campania.metricas.clicks + 50; // Arrays: métodos que mutan var canales = ["Google", "Meta"]; canales.push("TikTok"); // muta el original
Moderno — spread immutable
const campania = { nombre: "Summer Sale", activa: false, metricas: { clicks: 1200 } }; // ✓ Nueva copia, original intacto const campaniaActiva = { ...campania, activa: true, metricas: { ...campania.metricas, clicks: 1250 } }; // Arrays sin mutación const canales = ["Google", "Meta"]; const canalesActualizados = [...canales, "TikTok"];
Merge de configuraciones
const defaultConfig = { timeout: 5000, retries: 3, formato: "json", metricas: ["ctr", "roas"] }; const userConfig = { timeout: 10000, metricas: ["ctr", "cpa", "roas"] }; // userConfig gana (último spread) const config = { ...defaultConfig, ...userConfig }; // { timeout: 10000, retries: 3, formato: "json", metricas: [...] }
Rest en parámetros
// Acepta N canales sin array explícito const combinarMetricas = (nombre, ...canales) => ({ nombre, totalCanales: canales.length, clicksTotal: canales.reduce( (sum, c) => sum + c.clicks, 0 ) }); combinarMetricas( "Q4 2024", { canal: "Google", clicks: 4500 }, { canal: "Meta", clicks: 2800 }, { canal: "TikTok", clicks: 1900 } );
Async / Await // promesas · error handling
Callbacks → Async/Await
Legacy — callback hell
fetchCliente(123, function(err, cliente) { if (err) { console.error("Error cliente:", err); return; } fetchCampanias(cliente.id, function(err, camps) { if (err) { console.error("Error campanias:", err); return; } calcularROAS(camps, function(err, roas) { // Callback hell continúa... }); }); });
Moderno — async/await lineal
const getDashboardData = async (clienteId) => { try { const cliente = await fetchCliente(clienteId); // Paralelo: no hay dependencia entre sí const [campanias, presupuesto] = await Promise.all([ fetchCampanias(cliente.id), fetchPresupuesto(cliente.id) ]); return { cliente, campanias, presupuesto }; } catch (err) { console.error("Dashboard error:", err); throw err; } };
Retry con backoff exponencial
const fetchConRetry = async (url, reintentos = 3) => { for (let i = 0; i < reintentos; i++) { try { return await fetch(url); } catch (err) { if (i === reintentos - 1) throw err; // Espera: 1s, 2s, 4s… await new Promise( res => setTimeout(res, 1000 * 2 ** i) ); } } };
Promise combinators
const fuentes = [ fetchGoogleAds(clienteId), fetchMetaAds(clienteId), fetchTikTokAds(clienteId) ]; // all → falla si alguna falla const [google, meta, tiktok] = await Promise.all(fuentes); // allSettled → da resultado de cada una const resultados = await Promise.allSettled(fuentes); const exitosas = resultados .filter(r => r.status === "fulfilled") .map(r => r.value);
Programación Funcional // map · filter · reduce
Pipeline de transformación de datos de campañas
Legacy — loops imperativos
var campanias = [ { nombre: "Google Ads", gasto: 2000, ingresos: 8500, activa: true }, { nombre: "Meta Ads", gasto: 1500, ingresos: 3200, activa: true }, { nombre: "TikTok", gasto: 800, ingresos: 500, activa: false } ]; var activasConROAS = []; for (var i = 0; i < campanias.length; i++) { if (campanias[i].activa) { var c = campanias[i]; activasConROAS.push({ nombre: c.nombre, roas: c.ingresos / c.gasto }); } } activasConROAS.sort(function(a,b){ return b.roas - a.roas; });
Moderno — pipeline declarativo
const campanias = [ { nombre: "Google Ads", gasto: 2000, ingresos: 8500, activa: true }, { nombre: "Meta Ads", gasto: 1500, ingresos: 3200, activa: true }, { nombre: "TikTok", gasto: 800, ingresos: 500, activa: false } ]; const activasConROAS = campanias .filter(({ activa }) => activa) .map(({ nombre, gasto, ingresos }) => ({ nombre, roas: (ingresos / gasto).toFixed(2) })) .sort((a, b) => b.roas - a.roas);
Reduce — aggregación de métricas
const resumen = campanias.reduce((acc, c) => ({ ...acc, totalGasto: acc.totalGasto + c.gasto, totalIngresos: acc.totalIngresos + c.ingresos, numCampanias: acc.numCampanias + 1 }), { totalGasto: 0, totalIngresos: 0, numCampanias: 0 }); // ROAS global calculado al final const roasGlobal = resumen.totalIngresos / resumen.totalGasto;
Composición de funciones
// Funciones puras, composables const soloActivas = cs => cs.filter(c => c.activa); const conROAS = cs => cs.map(c => ({ ...c, roas: c.ingresos / c.gasto })); const ordenarPorROAS = cs => [...cs].sort((a,b) => b.roas - a.roas); // pipe: aplica fns de izq a der const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x); const procesarCampanias = pipe( soloActivas, conROAS, ordenarPorROAS );
Clases Modernas // private fields · ES2022
Clase con campos privados y métodos estáticos
class Campania { // Campos privados con # (ES2022) #gasto = 0; #ingresos = 0; #historial = []; // Campo estático compartido static roasMinimo = 2.0; constructor({ nombre, gasto, ingresos }) { this.nombre = nombre; this.#gasto = gasto; this.#ingresos = ingresos; } // Getter computado get roas() { return this.#ingresos / this.#gasto; } get rentable() { return this.roas >= Campania.roasMinimo; } // Método público que modifica privado actualizarGasto(nuevo) { this.#historial.push({ gasto: this.#gasto, ts: new Date() }); this.#gasto = nuevo; } // Factory method estático static fromAPI(data) { return new Campania({ nombre: data.name, gasto: data.spend, ingresos: data.revenue }); } } const c = new Campania({ nombre: "Black Friday", gasto: 5000, ingresos: 22000 }); console.log(c.roas); // 4.4 console.log(c.rentable); // true // console.log(c.#gasto) // ❌ SyntaxError — privado
Operadores Modernos // ?. · ?? · ??=
Optional chaining
Legacy
var ciudad = cliente && cliente.direccion && cliente.direccion.ciudad; var ctr = data && data.metricas && data.metricas.ctr;
Moderno
const ciudad = cliente?.direccion?.ciudad; const ctr = data?.metricas?.ctr; // También con llamadas a método const total = campania.calcular?.(); // Arrays const primero = arr?.[0];
Nullish coalescing + assignment
Legacy
// || falla con 0 y "" var ctr = data.ctr || 0; // Si ctr es 0: devuelve 0... pero // si es string vacío: devuelve 0 var n = config.nombre || "Sin nombre"; // Si nombre es "" devuelve "Sin nombre" // aunque fue asignado explícitamente
Moderno
// ?? solo hace fallback si null/undefined const ctr = data.ctr ?? 0; // 0 se conserva como 0 ✓ const n = config.nombre ?? "Sin nombre"; // "" se conserva como "" ✓ // Logical assignment config.timeout ??= 5000; // solo si null/undef obj.cache ||= {}; // si falsy obj.hits &&= obj.hits + 1; // si truthy
Módulos ES6 // import · export · dynamic
Estructura de módulos CultivaMetrics
analytics/campanias.js — exports
// Named exports — se pueden importar selectivamente export const calcularROAS = ({ ingresos, gasto }) => ingresos / gasto; export const calcularCPA = ({ gasto, conversiones }) => gasto / conversiones; export const ROAS_MINIMO = 2.0; // Default export — la función principal del módulo export default class CampaniaAnalytics { constructor(config) { this.config = config; } analizar(data) { /* ... */ } }
dashboard.js — imports
// Default + named en un import import CampaniaAnalytics, { calcularROAS, calcularCPA, ROAS_MINIMO } from "./analytics/campanias.js"; // Namespace import import * as Analytics from "./analytics/campanias.js"; // Dynamic import (code splitting) const cargarGraficas = async () => { const { ChartBuilder } = await import("./charts/index.js"); return new ChartBuilder(); };
15 Buenas Prácticas ES6+ — CultivaMetrics
1
const por defectoSolo usa let cuando necesites reasignar. Nunca var.
2
Arrow functions para callbacksPreserve el contexto de this sin .bind().
3
Template literalsNo más concatenación con +. Usa backticks y ${expr}.
4
Desestructura siempreParámetros, retornos de API, props — reduce repetición.
5
async/await sobre .then()Código más legible; usa try/catch para errores.
6
No mutes datosSpread operator y métodos que retornan nuevos arrays.
7
Optional chaining (?.) Elimina "Cannot read property of undefined".
8
Nullish coalescing (??)Usa ?? en vez de || para defaults cuando 0/"" son válidos.
9
Array methods funcionalesmap/filter/reduce sobre for loops — más expresivos.
10
Módulos ES6Una responsabilidad por módulo. Named exports rastreables.
11
Funciones purasSin side effects — más fáciles de testear y componer.
12
Nombres descriptivoscalcularROAS, no calc. Código autodocumentado.
13
Funciones pequeñasSingle Responsibility Principle — una tarea por función.
14
Errores con try/catchSiempre en funciones async. No silencies errores.
15
Campos privados (#)Usa # para encapsular estado interno en clases (ES2022).