Seleccionar página

En Javascript los objetos son siempre pasados por referencia entre una variable y otra, por lo que no es evidente la forma en la que se pueden copiar objetos y obtener una nueva entidad independiente. Dependiendo de que otros lenguajes de programación se han utilizado antes, esta característica puede parecer más o menos extraña, pero en todos los casos, este comportamiento suele provocar problemas y efectos colaterales no deseados, especialmente cuando, por ejemplo, una función modifica sin avisar un objeto que ha sido pasado cómo parámetro.

Por ejemplo, en este sencillo caso parece que estamos copiando el valor de a en b, pero lo que estamos haciendo es obtener otra nueva referencia al mismo objeto. De esta forma, cuando cambiamos a[0] también estamos cambiando b[0]:

const a = [1,2,3,4,5];
const b = a;

a[0] = 'uno';
console.assert(b[0] === 'uno');

En Javascript se pueden «copiar» objetos, aunque no dispongamos de un operador para ello. Vamos a ver paso a paso cómo se hace.

Operador de propagación (spread operator)

En ES2015 apareció el operador de propagación o spread operator (en inglés). Este operador facilita mucho la copia del contenido de un Array en otro:

const a = [1,2,3,4,5];
const b = [...a];
console.assert(a !== b);

En ES2018 se incluyó este mismo operador de propagación para los objetos literales y, por lo tanto, podemos con mucha facilidad copiar los valores de los objetos, es decir, las propiedades de uno en otro:

const x = {a: 1, b: 2, c: 3};
const y = {...x};
console.assert(x !== y);

En ambos casos, conseguimos lo que estábamos buscando: copiar los valores del objeto. Esta operación se puede realizar por otros medios, como el método .slice() de los Array o el método Object.assign(), pero creo que todos coincidiremos que el operador de propagación es muy sencillo de utilizar y bastante elegante.

Copia superficial vs. copia en profundidad

Cuando copiamos un objeto o una matriz, estamos haciendo una copia superficial de los valores del objeto, es decir, sólo estamos duplicando los valores del primer nivel de las propiedades del propio objeto y mantenemos intactos los objetos que pudieran existir a otros niveles de profundidad como objetos dentro de las propiedades.

const x = {a : {a : 1}, b : {b : 2}, c : {c : 3}};
const y = {...x};
console.assert (x.a !== y.a);	// Assertion failed

La solución es sencilla, para poder hacer es copiar objetos en profundidad debemos crear una pequeña función que de forma recursiva recorra los elementos del objeto y aplique el método de copia que estemos utilizando:

function copy (obj) {
  let result;
  if (obj instanceof Array) {
    result = [ ...obj ];
  } else if (typeof obj === 'object') {
    result = {...obj}
  } else {
    return obj;
  }
  for (let prop of Reflect.ownKeys (result)) {
    result[ prop ] = copy (result[ prop ]);
  }
  return result;
}

const a = [ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ];
const b = copy (a);
console.assert (a[ 0 ] !== b[ 0 ]);

const x = {a : {a : 1}, b : {b : 2}, c : {c : 3}};
const y = copy (x);
console.assert (x.a !== y.a);

Subclases que heredan de las clases predefinidas

Al hacer este tipo de funciones se nos suele olvidar que en ES2015 en adelante es posible crear clases derivadas de objetos predefinidos y que, por ejemplo, podemos crear una clase MyArray que herede de Array:

class MyArray extends Array {
  intersection(arr) {
    return new MyArray(...this.filter(value => arr.includes(value)));
  }
}

Si aplicamos la función copy() que hicimos en la sección anterior a este tipo de objeto, obtendremos un objeto Array y no un objeto MyArray:

const x = copy(a);
console.assert(x instanceof MyArray);    // Assertion failed

La solución es muy sencilla, sólo tenemos que cambiar de estrategia para hacer la creación del nuevo objeto utilizando el mismo constructor y realizar copia de los valores de sus propiedades. La función queda de esta forma:

function copy (obj) {
  let result;
  if (typeof obj === 'object') {
    result = new obj.constructor();
  } else {
    return obj;
  }
  for (let prop of Reflect.ownKeys (obj)) {
    result[ prop ] = copy (obj[ prop ]);
  }
  return result;
}

Con esta nueva función se permite copiar objetos Array, Object y las clases que hereden de ellos:

class MyArray extends Array {
  intersection(arr) {
    return new MyArray(...this.filter(value => arr.includes(value)));
  }
}

class MyObject extends Object {
  constructor() {
    super();
    this.class = 'MyModel';
  }
}

const a = new MyArray(1,2,3,4,5);
const b = copy(a);
console.assert(b instanceof MyArray);
console.assert(a != b);

const x = new MyObject();
const y = copy(x);
console.assert(y instanceof MyObject)
console.assert(a != b)

El caso especial de null

Aunque el código anterior tiene muy buen aspecto, lo cierto es que escode un problema importante: no funciona si el objeto es null. Este valor es un objeto, pero carece de constructor, por lo que la llamada a obj.constructor() lanzará una excepción.

const x = {n : null};
const y = copy (x);	// TypeError: Cannot read property 'constructor' of null

Bueno, sólo tenemos que incluir unas pocas líneas para controlar este caso:

function copy (obj) {
  let result;
  if (obj === null) {
    return obj;
  }
  if (typeof obj === 'object') {
    result = new obj.constructor ();
  } else {
    return obj;
  }
  for (let prop of Reflect.ownKeys (obj)) {
    result[ prop ] = copy (obj[ prop ]);
  }
  return result;
}

El objeto Date

Seguramente alguno de vosotros se habría dado cuenta, nos hemos olvidado del objeto Date. Las fechas en Javascript son un tipo de objeto y, por lo tanto, debemos encontrar una forma correcta para copiar objetos de tipo fecha y no pasarlos por valor al nuevo objeto. En nuestro código, aunque se llama a su constructor, no estamos estableciendo la fecha adecuada, ya que el valor de la fecha se debe incluir en la llamada al constructor y no basta con copiar el valor de las propiedades para igualar el valor interno de la fecha.

Nuevamente son unas pocas líneas más de código las que tenemos que incluir para controlar este nuevo caso, comprobando que estamos ante un objeto que hereda de Date:

function copy (obj) {
  let result;
  if (obj === null) {
    return obj;
  }
  if (obj instanceof Date) {
    result = new obj.constructor (obj.valueOf());
  } else if (typeof obj === 'object') {
    result = new obj.constructor ();
  } else {
    return obj;
  }
  for (let prop of Reflect.ownKeys (obj)) {
    result[ prop ] = copy (obj[ prop ]);
  }
  return result;
}

¡Alto! ¿pero cuantos tipos de objetos hay en Javascript?

Ahora que nos hemos percatado de que nos habíamos olvidado del objeto Date, podemos preguntarnos qué otros tipos de objetos nos hemos olvidado y que pueden ser susceptibles de ser copiados en Javascript, pueden estar contenidos en una matriz, en un objeto o simplemente necesitamos clonarlos. Para hacer una función consistente para copiar objetos deberíamos tener una respuesta razonable para todos los casos y no sólo para unos pocos.

Lo cierto es que la lista de tipos de objetos que podemos instanciar es bastante grande, mucho más de lo que solemos pensar. De forma resumida incluimos aquí esta referencia:

  • Objectos básicos:
    • Object, también se puede utilizar la expresión literal { }.
    • Array, también se puede utilizar la expresión literal [ ].
    • Date, objeto fecha.
  • Recubrimiento de los tipos primitivos, normalmente no se utilizan con new,
    pero si se hace, se crea un objeto que recubre el valor primitivo:
    • Boolean, recubre un tipo lógico.
    • Number, recubre un tipo numérico.
    • String, recubre un tipo cadena de texto.
  • Estructuras de datos:
    • Map, es una colección de pares clave/valor donde cualquier tipo puede usarse como clave o como valor..
    • Set, permite almacenar valores únicos de cualquier tipo, ya sean valores primitivos u objetos.
    • WeakMap, es una colección de pares clave/valor en el que las claves son siempre objetos y están débilmente referenciados.
    • WeakSet, permite almacenar objetos levemente referenciados.
  • Errores, son objetos de diferente tipo, aunque todos heredan de Error:
    • Error, error genérico.
    • EvalError, existe por compatibilidad hacia atrás
    • RangeError, error que indica que un valor no está dentro de rango aceptado.
    • ReferenceError, error que se produce cuando se usa una variable no creada.
    • SyntaxError, se produce cuando hay un error de sintaxis.
    • TypeError, se lanza cuando el tipo no es el esperado.
    • URIError, se lanza cuando se intenta utilizar una URI mal formada.
  • Funciones:
    • Function, rara vez se utiliza con new y casi siempre se utiliza una sintaxis como function () {} o () => {}.
    • GeneratorFunction, no se puede utilizar con new, siempre se utiliza una sintaxis function * () {}.
    • AsyncFunction, no se puede utilizar con new, siempre se utiliza una sintaxis async function () {}.
  • Expresiones regulares, para gestionar patrones en cadenas de texto:
    • RegExp, se puede crear también con la expresión literal / /.
  • Captura de operaciones con los objetos:
  • Control de ejecución:
    • Promise, devuelve un objeto para manejar la ejecución asíncrona de una función.
    • Array Iterator, no se puede crear directamente, se utiliza para iterar un Array.
    • AsyncGenerator, no se puede crear directamente, se devuelve por una función asíncrona y generadora.
    • Generator, no se puede crear directamente, se devuelve por las funciones generadoras.
    • String Iterator, no se puede crear directamente, se devuelve al iterar una cadena de texto.
    • Async Iterator, no se puede crear directamente, es utilizado en los bucles for wait of.
    • Map Iterator, no se puede crear directamente, se devuelve al iterar un Map.
    • Set Iterator, no se puede crear directamente, se devuelve al iterar un Set.
    • RegExp String Iterator, no se puede crear directamente, se devuelve al ejecutar el método .matchAll().
  • Manejo de datos binarios:
    • ArrayBuffer, crea un buffer de tamaño fijo para el manejo de datos binarios.
    • SharedArrayBuffer, crea un buffer de tamaño fijo compartido y no eliminable para el manejo de datos binarios.
    • DataView, permite manejar un ArrayBuffer o SharedArrayBuffer a bajo nivel.
    • TypedArray, no se puede crear directamente, de él heredan los siguientes objetos:
      • BigInt64Array, manejo del buffer como enteros de 64 bits.
      • BigUint64Array, manejo del buffer como enteros de 64 bits sin signo.
      • Float32Array, manejo del buffer como número de coma flotante de 32 bits.
      • Float64Array, manejo del buffer como número de coma flotante de 64 bits.
      • Int8Array, manejo del buffer como enteros de 8 bits.
      • Int16Array, manejo del buffer como enteros de 16 bits.
      • Int32Array, manejo del buffer como enteros de 32 bits.
      • Uint8Array, manejo del buffer como enteros de 8 bits sin signo.
      • Uint8ClampedArray, manejo del buffer como enteros de 8 bits sin signo con valores entre 0 y 255.
      • Uint16Array, manejo del buffer como enteros de 16 bits sin signo.
      • Uint32Array, manejo del buffer como enteros de 32 bits sin signo.
  • Objectos globales, de los que no se puede crear una nueva instancia:
    • Atomics, contiene métodos para el manejo de un ShareArrayBuffer.
    • JSON, contiene métodos para la interpreación y creación de cadenas de texto con formato JSON.
    • Math, contiene métodos y constantes matemáticas.
    • Reflect, contiene métodos para reproducir operaciones interceptadas, habitualmente en un Proxy.
    • globalThis, corresponde con window en los navegaores y con root en Node y es donde residen las variables globales.
    • null, objeto con valor null
    • Intl, contiene los objetos del ECMAScript Internationalization API.

A estas clases de objetos se deberían añadir los tipos de objetos que creemos nosotros con class, ya sea heredando de Object o de cualquiera de los constructores predefinidos. La naturaleza y diseño de estas clases puede ser muy variado y hay que tener en cuenta las características concretas de cada uno.

También es importante tener en cuenta dos situaciones peculiares que se pueden producir al utilizar Object.create()

  • Object.create({ }), devuelve un objeto cuyo constructor es Object pero cuyo prototipo es el objeto pasado como parámetro.
  • Object.create(null), devuelve un objeto que no hereda de Object y que tiene como prototipo null, pero carece de constructor.

Estos objetos tienen un comportamiento especial y debemos conocerlo.

También tenemos que recordar que los objetos pueden hacer referencia a si mismos, es decir, tener referencias circulares. Si no recordamos está característica seguramente tengamos algunos problemas.

Por ultimo, tenemos que recordar que las propiedades de los objetos se pueden definir y configurar con Object.defineProperty(), indicando si son enumerables o no, si son de sólo lectura o están gestionadas con un método get y un método set(), etc. Cuando trabajemos con las propiedades es conveniente que utilicemos sus descriptores y no sólo los creemos directamente.

Ahora tenemos que volver a preguntarnos, ¿cómo es posible copiar objetos de todos estos tipos teniendo en cuenta sus diferentes peculiaridades y características?

Una función para copiarlos a todos

Aunque parezca difícil, lo cierto es que podemos crear con bastante facilidad una función para copiar objetos correctamente independientemente del tipo que sean y de las características de que dispongan. Vamos a ver cómo.

En primer lugar, tenemos que aclarar que algunos objetos no tiene mucho sentido que sean copiados. Por ejemplo, copiar funciones (que son objetos) no parece tener mucha utilidad. Podríamos obtener su código y volverlo a evaluar, pero no es algo que tenga mucho sentido en la práctica. Tampoco tiene sentido que copiemos los objetos que se utilizan para gestionar el control de ejecución como Promise, los objetos que manejan los iteradores o los devueltos por las funciones generadoras. El estado de estos objetos debe mantenerse de forma única y obtener un duplicado sólo puede producir problemas. Por último, tenemos que comprender que no se pueden copiar objetos de tipo WeakMap y WeakSet , ya que carecemos de mecanismos para recorrer su contenido y sólo podemos acceder a sus elementos si conocemos previamente las claves que contienen.

También tenemos que tener en cuenta que copiar objetos de clases personalizadas puede llegar a ser bastante complejo, sobre todo si establecen constructores que reciben los datos por parámetro y no reflejan los mismos en propiedades públicas que podamos copiar. Esto ocurre tanto en clases que heredan de Object como las que heredan de otros constructores. Para estos casos vamos a crear un símbolo bien conocido para implementar en nuestras clases un método que permita la copia de los objetos. Si la clase implementa un método con el símbolo, se utilizará para hacer la copia del objeto. No es una solución universal, pero al menos podemos aplicarla en nuestras clases.

Ahora que tenemos claro que cosas no vamos a copiar y que limitaciones tenemos, podemos mostrar el código de esta función que puede copiar objetos de cualquier tipo:

const copy = (function () {
  const TypedArray           = Object.getPrototypeOf (Int32Array);
  const hasNode              = typeof Node !== 'undefined';
  const hasSharedArrayBuffer = typeof SharedArrayBuffer !== 'undefined';
  const ignoreTypes          = [ 'Array Iterator', 'AsyncGenerator', 'Generator',
    'String Iterator', 'Async Iterator', 'Map Iterator', 'Set Iterator',
    'RegExp String Iterator', 'Promise', 'WeakMap', 'WeakSet' ];

  const stack = new Map();                                 // Stack for circular objects
  function copy (obj) {
    if (typeof obj !== 'object') {
      return obj;
    }
    if (obj === null ||                                    // null or ignore object type
        ignoreTypes.includes (obj[ Symbol.toStringTag ]))
    {
      return obj;
    }
    if (stack.has(obj)) {                                  // Check if it's a circular object
      return stack.get(obj);
    }
    let result;
    let proto = Object.getPrototypeOf (obj);
    if (obj[ copy.symbol ]) {                              // Well kwon symbol for copy
      result = obj[ copy.symbol ] ();
    } else if (proto === Object.prototype) {               // Simple object
      result = {};
    } else if (proto === null) {                           // Create.object(null)
      result = Object.create (null);
    } else if (obj instanceof Date ||                      // Date, Boolean, String, Number
               obj instanceof Boolean ||
               obj instanceof Number ||
               obj instanceof String)
    {
      result = new (obj.constructor) (obj.valueOf ());
    } else if (obj instanceof Map) {                       // Map
      result = new (obj.constructor) ();
      for (let key of obj.keys ()) {
        result.set (key, copy (obj.get (key)));
      }
    } else if (obj instanceof Set) {                       // Set
      result = new (obj.constructor) ();
      for (let value of obj.values ()) {
        result.add (copy (value));
      }
    } else if (obj instanceof ArrayBuffer ||               // ArrayBuffer, SharedArrayBuffer
               (hasSharedArrayBuffer && obj instanceof SharedArrayBuffer))
    {
      result = obj.slice (0);
    } else if (obj instanceof DataView) {                  // DataView
      result = new (obj.constructor) (obj.buffer.slice (0));
    } else if (obj instanceof TypedArray) {                // All typed array
      result = new obj.constructor (obj);
    } else if (obj instanceof RegExp) {                    // RegExp
      result = new (obj.constructor) (obj.source, obj.flags || obj.options);
    } else if (obj instanceof Error) {                     // Error and derived objects
      result = new (obj.constructor) (obj.toString ());
      if (obj.stack) {
        result.stack = obj.stack;                          // Firefox fix
      }
    } else if (hasNode && obj instanceof Node) {           // HTML Node
      result = obj.cloneNode (true);
    } else if (obj.constructor === Object) {               // Custom propotype
      result = Object.create(proto);
    } else {                                               // Array or other objects
      result = new (obj.constructor) ();
    }
    stack.set(obj, result);                                // Update stack for circular objects
    for (let key of Reflect.ownKeys (obj)) {               // Properties
      const descrOrigin = Object.getOwnPropertyDescriptor (obj, key);
      const descrResult = Object.getOwnPropertyDescriptor (result, key);
      if (!descrResult || descrResult.configurable) {
        if (descrOrigin.set || descrOrigin.set) {          // descriptor with getter and setter
          Object.defineProperty ( result, key,
            Object.assign (descrOrigin, {
              set : (descrOrigin.set ? descrOrigin.set.bind (result) : undefined),
              get : (descrOrigin.get ? descrOrigin.get.bind (result) : undefined)
            })
          );
          result[ key ] = copy (obj[ key ]);
        } else {                                           // descriptor with value
          Object.defineProperty ( result, key,
            Object.assign (descrOrigin, {value : copy (obj[ key ])})
          );
        }
      }
    }
    return result;
  }
  
  copy.symbol = Symbol ('method copy');

  return copy;
  
})();

Son unas pocas líneas de código, y seguramente se explican por si solas, pero vamos a explicar que estamos haciendo paso a paso:

  • Se crea una constante copy con el retorno de una función de ejecución inmediata.
  • Se crean unas constantes:
    • Obtenemos el constructor de Int32Array para poder identificar todos los Typed Array
    • Comprobamos si Node está disponible (sólo lo estará en los navegadores)
    • Comprobamos si SharedArrayBuffer está disponible, ya no todas las implementaciones de Javascript lo han incorporado
    • Creamos una lista de descriptores de objetos que vamos a ignorar.
    • Creamos un objeto Map donde vamos a guardar los objetos que ya hemos procesado para gestionar adecuadamente las referencias circulares.
  • Se define la función copy:
    • Si no es un objeto, entonces sería un valor primitivo o una función, se devuelve el valor original
    • Si es null o uno de los tipos que no se van a copiar, se devuelve el valor original
    • Si ya ha sido proceso está dentro del objeto Map y para evitar procesarlo en bucle infinito devolvemos el objeto que se incluyó en esa pila
    • Si el objeto tiene un método definido con copy.simbol, se llama a ese método para obtener una copia del objeto
    • Si el prototipo del objeto es el mismo que Object, entonces es un objeto simple que podemos crear con el literal {}
    • Si el prototipo del objeto es null, entonces el objeto se creó con Object.create(null)
    • Si el objeto está heredando de Date, Boolean, String o Number, se llama a su constructor pasando el valor interno obtenido con .valueOf()
    • Si el objeto está heredando de Map, se llama al constructor y se insertan los valores con el método .set()
    • Si el objeto está heredando de Set, se llama al constructor y se insertan los valores con el método .add()
    • Si el objeto está heredando de ArrayBuffer o SharedArrayBuffer se obtiene una copia por medio del método .slice()
    • Si el objeto está heredando de DataView, se llama al constructor pasando como parámetro una copia del buffer que está manejando
    • Si el objeto está heredando de TypeArray, se llama al constructor pasando como parámetro el propio objeto
    • Si el objeto está heredando de RegExp, se llama al constructor pasando la expresión regular obtenido de la propiedad .source y los modificadores por medio de la propiedad .flags u .optionsya que dependiendo de la implementación está en una propiedad o en otra
    • Si el objeto está heredando de Error, se llama al constructor pasando el mensaje como parámetro y copiando la propiedad .stack en Firefox
    • Si el objeto está heredando de Node, estamos en un navegador y copiamos el elemento con el método .cloneNode()
    • Si el constructor es Object, pero hemos llegado hasta aquí, quiere decir que el prototipo no es el del Object, por lo que es el caso especial de creación con Object.create({ })
    • En cualquier otro caso, es un Array o un objeto creado a partir de una clase
    • Para todos los objetos, se recorren sus propiedades propias, enumerables y no enumerables, y
      • Se obtiene el descriptor de la propiedad para el objeto original y el que hemos creado
      • Si en el objeto creado se permite la modificación de la propiedad
        • Si el descriptor es de tipo setter/getter
          • se crea con esa estructura en el objeto
          • se asigna el valor llamando de forma recursiva a copy()
        • Si el descriptor es de tipo value, se llama asigna el valor de forma recursiva a copy()
    • Se retorna el nuevo objeto
  • Creamos un Symbol para poder utilizarlo como nombre del método de copia en nuestras clases.
  • Se retorna la función copy

Conclusiones

Cuando empezamos parecía que todo sería muy sencillo, que un operador y algunas líneas en una pequeña función nos iban a resolver todos nuestros problemas a la hora de copiar objetos. Lo cierto es que en muchos casos esta aproximación simplificada cubre la mayoría de las situaciones y puede ser razonable su uso. Intencionadamente hemos querido empezar de esta forma, cómo suelen iniciarse el desarrollo de este tipo de funciones, partiendo de unos pocos casos sencillos.

En la mayoría de las ocasiones vamos a darnos cuenta rápidamente que tenemos que dar respuesta a más situaciones de las que habíamos pensado inicialmente, especialmente si lo que estamos es buscando una función verdaderamente robusta, que de respuesta a todos los casos. El Javascript moderno ofrece una buena cantidad de tipos de objetos y algunos casos bastante peculiares que hacen que los objetos se comporten de forma diferente. No es demasiado complicado dar respuesta a todos ellos. Quizás pueda resultar un poco tedioso analizar caso por caso, pero existen respuestas para todos los casos, sólo tenemos que aplicarnos un poco para ser exhaustivos.

Si vais a abordar alguna función global para el manejo de objetos os recomendamos que tengáis a mano una lista de objetos posibles en Javascript y comprobéis como funciona vuestro código en cada caso. Os podréis sorprender en bastantes situaciones de lo poco preparados que estamos para responder a a un simple objeto Date,  a un objeto Map o a una referencia circular. No hay que agobiarse, sólo tener en cuenta que el lenguaje ha crecido y tiene un mayor número de tipos de objetos a los que nos tenemos que enfrentar. Con un poco de práctica conseguiremos dar respuesta general a todos los casos que se nos presenten.

 

Novedades

HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)

HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)

En esta charla, organizada por MadridJS, Pablo Almunia nos muestra cómo la mayoría de nosotros cuando oímos hablar por primera vez de HTTP2 nos ilusionamos con las posibilidades que presumiblemente se abrían para el desarrollo de soluciones web avanzadas y cómo muchos nos sentimos defraudados con lo que realmente se podía implementar.

En esta charla podemos ver cómo funciona el HTTP2, que debemos tener en cuenta en el servidor para hace uso de este protocolo y, sobre todo, cómo podemos enviar información desde el servidor al cliente de forma efectiva y fácil. Veremos con detenimiento cómo por medio de los Server-Sent Events (SSE) podemos recibir en el cliente datos enviados desde el servidor sin utilizar websocket, simplificando enormemente la construcción de aplicaciones con comunicación bidireccional.

Observables en Javascript con Proxies

Observables en Javascript con Proxies

En esta charla, organizada por MadridJS, Pablo Almunia nos habla de la observación reactiva de objetos en Javascript por medio de Proxy. Se describe paso a paso cómo funcionan los Proxies y en que casos pueden ser nuestro mejor aliado. Veremos que no hay que tenerles miedo, son bastante sencillos de utilizar, y nos ofrecen una gran abanico de posibilidades.

Aplicaciones JAMStack, SEO friendly y escalables con NextJS

Aplicaciones JAMStack, SEO friendly y escalables con NextJS

En esta charla de Madrid JS, Rafael Ventura nos describe las funcionalidades clave de NextJS, nos muestra en vivo cómo desarrollar una completa aplicación JAMStack con Server Side Rendering (SSR) y Static Site Generation (SSG) y termina mostrando como publicar esta aplicación en Vercel.

Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera

Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera

En esta charla Rubén Aguilera nos cuenta los problemas que tienen muchas empresas a la hora de sacar productos accesibles, vistosos y usables en el Time To Market que requiere Negocio y cómo podemos minimizar este tiempo gracias al DevUI con StencilJS para adecuar una aplicación de Angular a las exigencias del mercado en tiempo record.