Inicio / Artículos

Auditando utils.js: Seis funciones integradas que reemplazan a los auxiliares manuales

Descubra qué APIs nativas de JavaScript pueden reemplazar los trucos de clonación profunda, las copias defensivas, los tiempos de espera manuales, las promesas diferidas y lodash groupBy, así como qué consideraciones son aplicables.

Editor: REACTAPP.TOP5 min de lectura1052 palabras

Fuentes: 6 Modern JavaScript Features You’re Probably Still Working Around

Muchos conjuntos de código siguen incluyendo funciones auxiliares e importaciones de bibliotecas para problemas que la plataforma ya resolvió hace años. Estas adiciones se integraron de forma silenciosa, por lo que las costumbres formadas alrededor de estas soluciones antiguas nunca fueron revisadas. A continuación se presentan seis funciones integradas que vale la pena verificar en su utils.js, junto con los problemas que cada una resuelve y las limitaciones que debe conocer antes de eliminar algo.

structuredClone() en lugar del proceso de ida y vuelta con JSON

La forma más común de crear una copia profunda casera consiste en serializar un objeto y luego volver a parsearlo:

const copy = JSON.parse(JSON.stringify(original));

Este método falla con cualquier cosa que JSON no pueda representar: las instancias de Date vuelven como cadenas de texto, las propiedades establecidas en undefined se eliminan, Map y Set se convierten en objetos vacíos sin ninguna advertencia, y una referencia circular hace que JSON.stringify lance un error.

structuredClone() es una función global disponible en los navegadores modernos y en Node.js, sin necesidad de importar nada. Tomemos un objeto que contiene una fecha, un conjunto y un mapa:

const original = {
  date: new Date(),
  items: new Set([1, 2, 3]),
  meta: new Map([['key', 'value']]),
};

Al clonarlo, cada uno de estos tipos se mantiene intacto, y las estructuras cíclicas también se copian correctamente:

const copy = structuredClone(original);
// Dates, Sets, Maps, and circular refs all survive intact

Utiliza el algoritmo de clonado estructurado que está detrás de postMessage e IndexedDB, por lo que abarca casos límite que los clonados manuales pasan por alto. Sin embargo, tiene limitaciones: las funciones y los nodos DOM no pueden ser clonados y generan un error, además, las instancias de tus propias clases vuelven como objetos simples sin su prototipo.

toSorted() y toReversed() en lugar de copias defensivas

.sort() y .reverse() mutan en el mismo lugar, por lo que ordenar un array compartido por referencia lo modifica en todas partes. Partiendo de este array:

const scores = [30, 10, 20];

toSorted() devuelve un nuevo array ordenado y deja el original sin cambios:

const sorted = scores.toSorted((a, b) => a - b);console.log(scores); // [30, 10, 20] — untouched
console.log(sorted);  // [10, 20, 30]

toReversed() y toSpliced() siguen el mismo patrón de devolución mediante copia. Si su código está repleto de [...arr].sort() para mantener el estado inmutable, como suelen hacer los reducers de React, ya no es necesario utilizar la expansión. Se encuentra una explicación más detallada sobre estos métodos que devuelven una copia en métodos modernos de arrays en JavaScript que reemplazan patrones genéricos.

Object.hasOwn() en lugar de hasOwnProperty

Llamar al método a través del objeto oculta una trampa:

obj.hasOwnProperty('key'); // breaks if obj = Object.create(null)

Un objeto creado con Object.create(null) no tiene prototipo, por lo que nunca hereda hasOwnProperty y la llamada lanza un TypeError. Lo mismo ocurre si un objeto define su propia propiedad con ese nombre. Object.hasOwn() es una función estática, por lo que no depende de lo que herede el objeto:

Object.hasOwn(obj, 'key'); // works regardless of the object's prototype chain

El cambio es mínimo, pero evita fallos que solo aparecen en producción, cuando un llamante pasa una estructura de objeto que nadie había previsto.

AbortSignal.timeout() en lugar de un temporizador manual

Establecer una fecha límite para fetch solía requerir un AbortController junto con un setTimeout que lo activara:

const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 5000);

Luego había que recordar borrar el temporizador una vez que la solicitud finalizaba:

fetch(url, { signal: controller.signal }).finally(() => clearTimeout(id));

El signal ahora puede tener su propia fecha límite:

fetch(url, { signal: AbortSignal.timeout(5000) });

Una sola línea, sin limpieza, sin temporizador restante. Cuando se dispara, la solicitud es rechazada con un TimeoutError DOMException, lo que permite distinguir un tiempo de espera de una cancelación provocada por el usuario. Para combinar un tiempo de espera con otra fuente de cancelación, consulte AbortSignal.any() en la documentación actual.

Promise.withResolvers() en lugar de usar resolve y reject de forma no controlada

A veces es necesario resolver una promesa desde fuera de su ejecutor. La solución clásica consiste en declarar variables y asignárselas dentro de la función de callback del constructor:

let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

Funciona, pero pasar funciones fuera de un cierre es fácil de interpretar erróneamente. Promise.withResolvers() devuelve la promesa junto con ambas funciones de resolución:

const { promise, resolve, reject } = Promise.withResolvers();

Esto encaja perfectamente cuando se pone en cola trabajo asíncrono o se envuelve una API basada en eventos en promesas. Para un ejemplo más completo, consulte coordinar llamadas a herramientas de LLM con Promise.withResolvers.

Object.groupBy() en lugar de lodash groupBy

El agrupamiento de registros por una clave es una de las razones más frecuentes para importar una biblioteca de utilidades. Dada una lista de personas:

const people = [
  { name: 'Sam', age: 21 },
  { name: 'Lee', age: 34 },
  { name: 'Kai', age: 21 },
];

Object.groupBy() acepta un iterable y una función de callback, y devuelve un objeto con un array por cada clave:

const byAge = Object.groupBy(people, (p) => p.age);
// { 21: [Sam, Kai], 34: [Lee] }

Tenga en cuenta el nombre. Una propuesta anterior lo denominaba Array.prototype.group(), pero fue renombrado y trasladado a la función estática Object.groupBy() (además de Map.groupBy() cuando las claves no son cadenas) antes de la estandarización; solo se incluyen las versiones estáticas. El resultado tiene un prototipo nulo, y se trata de la API más reciente en esta lista, por lo que verifique las versiones mínimas de navegador y Node.js soportadas antes de eliminar la dependencia.

Antes de eliminar las funciones auxiliares

Ninguno de estos cambios afecta su arquitectura, pero cada uno elimina un patrón propenso a errores o una dependencia innecesaria. Un orden sensato:

  • Verifique el soporte en caniuse.com o MDN en relación con los entornos a los que realmente apunta, ya que algunos de ellos son más recientes que otros y los navegadores antiguos pueden no contar con ellos.
  • Mantenga los polyfills hasta que su matriz de soporte realmente permita eliminarlos.
  • Sustituya gradualmente los puntos de llamada, realizando pruebas en torno a la clonación y el agrupamiento cuando sea importante la forma del resultado.
  • Elimine la importación del antiguo helper o biblioteca solo una vez que ya no haya nada que lo haga referencia.
  • Lecturas relacionadas