Hasta ahora, TaskFlow ha trabajado siempre con datos ya disponibles de antemano: el array tareas de <task-board> se ha inicializado directamente en el constructor, con valores de ejemplo escritos a mano, listos para renderizarse desde el primer momento. Ningún proyecto real funciona así: las tareas de una aplicación de gestión llegarían de una API, de una base de datos local, o de cualquier otra fuente que tarda un tiempo —normalmente indeterminado— en responder. Esta lección resuelve ese problema con la directiva until, y aprovecha para conectar esa solución con el mixin ConEstadoCarga del módulo 6, que ya apuntaba en la misma dirección con una herramienta distinta.

Contenido

  1. El problema: mostrar datos que todavía no han llegado
  2. La directiva until y su firma básica
  3. Simulando una carga real: cargarTareas()
  4. Aplicando until a <task-board> con un esqueleto de carga
  5. Un error habitual: crear la promesa dentro de render()
  6. until con varios valores: prioridad por posición
  7. until frente a ConEstadoCarga: cuándo conviene cada uno

  1. El problema: mostrar datos que todavía no han llegado

render(), tal como se ha estudiado desde el módulo 2, siempre debe ejecutarse de forma síncrona: recibe el estado actual del componente y devuelve inmediatamente una plantilla, sin esperar a nada. Esta restricción no es negociable ni tiene ninguna vía de escape especial: render() nunca puede ser una función async, ni puede devolver directamente una Promise para que Lit "espere" a que se resuelva antes de pintar algo en pantalla.

El problema, entonces, es evidente: si <task-board> necesitara cargar sus tareas desde una función asíncrona (cargarTareas(), que tarda un tiempo en resolver), ¿qué debería devolver render() mientras esa promesa todavía está pendiente? Sin ninguna herramienta adicional, la única opción sería recurrir a alguna variante de lo ya visto en el módulo 6: guardar el resultado en una propiedad de estado (this.tareas), inicializarla vacía, y actualizarla cuando la promesa resuelva, dejando que el nuevo valor dispare un nuevo render() por el mecanismo habitual de propiedades reactivas. Esa solución funciona, y de hecho es la que se ha usado implícitamente hasta ahora sin nombrarla; el mixin ConEstadoCarga de la lección 06-04 añadía, sobre esa misma base, una propiedad cargando explícita para mostrar un aviso mientras la espera dura. Esta lección presenta una alternativa más declarativa, pensada específicamente para este problema: expresar la espera directamente dentro de la plantilla, sin necesidad de una propiedad de estado intermedia dedicada solo a saber si algo sigue cargando.

  1. La directiva until y su firma básica

import { until } from 'lit/directives/until.js';

html`${until(promesa, contenidoDeRepuesto)}`;

until recibe, en su uso más simple, dos argumentos: una Promise y un valor de repuesto (normalmente otra plantilla html, aunque puede ser cualquier valor renderizable). Mientras la promesa no se haya resuelto, until muestra el contenido de repuesto; en el instante en que la promesa resuelve, until sustituye automáticamente ese contenido por el valor resuelto, sin que el componente que la usa tenga que gestionar manualmente ninguna propiedad de estado para saber en qué momento ocurre esa sustitución. Por dentro, until no es más que otra directiva personalizada, construida exactamente con las mismas piezas —directive() y Directive— que se han presentado en la lección anterior: mantiene su propio estado interno (qué valor mostrar en cada momento) y usa part para actualizar la posición del DOM cuando la promesa resuelve, sin que su usuario necesite conocer nada de ese detalle interno para aprovecharla.

  1. Simulando una carga real: cargarTareas()

Para poder aplicar until a un caso real de TaskFlow, hace falta primero algo que simule la espera de una fuente de datos externa. Sin entrar todavía en peticiones de red reales (eso corresponde al módulo 8), basta con una función que devuelva una Promise que se resuelve tras un breve retraso simulado con setTimeout:

// src/services/tareas-service.js
export function cargarTareas() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 't1', titulo: 'Diseñar la base de datos', estado: 'hecha', prioridad: 2 },
        { id: 't2', titulo: 'Implementar autenticación', estado: 'progreso', prioridad: 3 },
        { id: 't3', titulo: 'Escribir pruebas de integración', estado: 'pendiente', prioridad: 1 },
      ]);
    }, 1200);
  });
}

Esta función no forma parte del propio componente <task-board>; vive en un módulo de servicio separado, siguiendo la misma idea de separación de responsabilidades que ya ha aparecido en otros puntos del curso (los controladores del módulo 6, por ejemplo): <task-board> no necesita saber cómo se obtienen las tareas, solo que cargarTareas() devuelve una promesa que eventualmente resuelve con un array de tareas.

  1. Aplicando until a <task-board> con un esqueleto de carga

Con la simulación ya lista, <task-board> guarda la promesa devuelta por cargarTareas() una única vez, y la transforma con .then() en una promesa que resuelve directamente a una plantilla, lista para pasarse a until:

// src/components/task-board.js
import { LitElement, html, css } from 'lit';
import { until } from 'lit/directives/until.js';
import { ConEstadoCarga } from '../mixins/con-estado-carga.js';
import { cargarTareas } from '../services/tareas-service.js';
import './task-list.js';

class TaskBoard extends ConEstadoCarga(LitElement) {
  static properties = {
    ...ConEstadoCarga(LitElement).properties,
    tareas: { type: Array },
  };

  constructor() {
    super();
    this.tareas = [];
    this._tareasTemplate = cargarTareas().then((tareas) => {
      this.tareas = tareas;
      return html`
        <task-list .tareas="${tareas}" @tarea-cambiada="${this.gestionarTareaCambiada}"></task-list>
      `;
    });
  }

  renderEsqueleto() {
    return html`
      <div class="esqueleto" aria-busy="true" aria-label="Cargando tareas">
        <div class="esqueleto__linea"></div>
        <div class="esqueleto__linea"></div>
        <div class="esqueleto__linea"></div>
      </div>
    `;
  }

  render() {
    return html`
      <div class="tablero">
        <h1>TaskFlow</h1>
        ${until(this._tareasTemplate, this.renderEsqueleto())}
      </div>
    `;
  }

  static styles = css`
    .esqueleto__linea {
      height: 3rem;
      margin-bottom: 0.5rem;
      border-radius: 4px;
      background: linear-gradient(90deg, #e0e0e0 25%, #ececec 50%, #e0e0e0 75%);
      background-size: 200% 100%;
      animation: pulso 1.4s ease-in-out infinite;
    }

    @keyframes pulso {
      0% { background-position: 200% 0; }
      100% { background-position: -200% 0; }
    }
  `;
}

customElements.define('task-board', TaskBoard);

El punto central de este código es this._tareasTemplate, construida en el constructor con cargarTareas().then((tareas) => { ... }): en lugar de guardar la promesa "en bruto" de cargarTareas() (que resolvería con un array de datos), se encadena un .then() que hace dos cosas a la vez —actualizar this.tareas como efecto secundario, por si algún otro punto del componente necesitara leer ese array directamente, y devolver ya una plantilla html con <task-list> completamente montada—. El resultado, this._tareasTemplate, es una promesa que resuelve directamente a algo renderizable, exactamente lo que until espera como primer argumento.

En render(), until(this._tareasTemplate, this.renderEsqueleto()) muestra el esqueleto de carga (tres barras con una animación de pulso, definida en static styles) mientras la promesa siga pendiente, y lo sustituye automáticamente por <task-list> en el instante en que cargarTareas() resuelve, sin que <task-board> haya tenido que declarar ninguna propiedad cargando para esta operación en concreto.

  1. Un error habitual: crear la promesa dentro de render()

El detalle más importante de todo el ejemplo anterior, fácil de pasar por alto, es que this._tareasTemplate se crea una única vez, en el constructor, no dentro de render(). Escribir esto en su lugar sería un error grave:

// Incorrecto: no hacer esto
render() {
  return html`
    <div class="tablero">
      <h1>TaskFlow</h1>
      ${until(cargarTareas().then((tareas) => html`<task-list .tareas="${tareas}">...</task-list>`), this.renderEsqueleto())}
    </div>
  `;
}

render() puede ejecutarse muchas veces durante la vida de un componente, cada vez que cambia cualquier propiedad reactiva (como se explicó en la lección 02-05). Si cargarTareas() se llamara dentro de render(), cada nueva ejecución de render() —incluso una provocada por un cambio completamente ajeno a la carga de tareas— dispararía una nueva llamada a la función, iniciando una nueva simulación de red desde cero y mostrando de nuevo el esqueleto de carga mientras esa nueva promesa esté pendiente, en un bucle de destellos que nunca llegaría a estabilizarse del todo. Por eso this._tareasTemplate se calcula una única vez, en el constructor (el lugar adecuado para cualquier operación que deba ocurrir exactamente una vez en la vida del componente, como ya se vio con los controladores reactivos y los mixins en el módulo 6), y render() se limita a leer esa misma referencia, ya estable, en cada ejecución.

  1. until con varios valores: prioridad por posición

until admite, además de la forma de dos argumentos ya vista, cualquier número de valores, no necesariamente todos promesas:

html`${until(promesaLenta, promesaRapida, contenidoDeRepuesto)}`;

Con varios argumentos, until los trata con un orden de prioridad estricto según su posición, no según el orden en que resuelven: mientras ninguno haya resuelto todavía, se muestra el último argumento (el de menor prioridad, normalmente un valor síncrono, no una promesa); en cuanto resuelve cualquiera de los argumentos anteriores, se muestra su valor, pero en cuanto resuelve uno de posición más alta (más a la izquierda), until cambia a mostrar ese en su lugar, y ya no vuelve a los de menor prioridad aunque estos actualicen su valor más adelante. Esto permite construir una carga progresiva con más de un nivel de detalle —por ejemplo, un resumen rápido calculado en el propio navegador mientras se espera la respuesta completa de una fuente más lenta—, algo que TaskFlow no necesita todavía con una única fuente de datos como cargarTareas(), pero que conviene conocer para reconocer el patrón si aparece en código de terceros o en necesidades futuras del proyecto.

  1. until frente a ConEstadoCarga: cuándo conviene cada uno

Con las dos herramientas ya disponibles para el mismo problema general —mostrar algo mientras se espera una operación asíncrona—, conviene fijar el criterio para elegir entre ellas en cada situación futura de TaskFlow:

Criterio until ConEstadoCarga (mixin, lección 06-04)
Dónde vive la lógica de espera Declarada directamente en la plantilla, en el punto exacto donde se necesita En una propiedad cargando del propio componente, gestionada explícitamente
¿El estado de carga es visible fuera de esa posición de la plantilla? No, de forma directa; solo afecta a esa expresión concreta Sí: this.cargando puede leerse y usarse en cualquier parte del componente (por ejemplo, para deshabilitar un botón)
¿Encaja bien con un valor que llega una sola vez? Sí, es su caso de uso principal También, pero requiere gestionar manualmente el cambio de true a false
¿Encaja bien con una operación que se repite muchas veces (guardar, reintentar)? Peor: cada nueva operación exige una nueva promesa y, con cuidado, volver a mostrar un estado de "pendiente" Mejor: basta con alternar this.cargando entre true y false tantas veces como haga falta
Ejemplo de este curso Carga inicial de tareas en <task-board>, un único valor esperado una vez Cualquier operación futura que necesite comunicar su estado de carga a varias partes del componente, o repetirse varias veces

El criterio de fondo es que until es una solución declarativa, ligada a una única posición de la plantilla y a una única promesa concreta, ideal para el caso de esta lección: un valor que se espera una vez y que, en cuanto llega, sustituye limpiamente a su contenido de repuesto. ConEstadoCarga, en cambio, expone su estado como una propiedad normal del componente, más flexible para operaciones que se repiten en el tiempo o que necesitan comunicar su estado de carga más allá de una única posición de una única plantilla. Ambas técnicas son complementarias, no mutuamente excluyentes: <task-board> sigue extendiendo ConEstadoCarga(LitElement) en el ejemplo del apartado 4, disponible para cualquier futura operación de TaskFlow (como guardar cambios) que sí encaje mejor con ese patrón, mientras que la carga inicial de tareas, al ser una operación de una sola vez, usa until en su lugar.

Errores Comunes y Consejos

  • Crear la promesa dentro de render() en lugar de en el constructor: como se ha explicado en el apartado 5, esto reinicia la operación asíncrona en cada renderizado, provocando destellos repetidos del contenido de repuesto y, en un caso real con una petición de red, peticiones duplicadas innecesarias.
  • Esperar que until funcione con una función async como render(): render() nunca puede declararse async ni devolver una Promise directamente; until es precisamente el mecanismo que permite mantener render() síncrono mientras se representa, dentro de una posición concreta de la plantilla, el resultado de una operación que sí es asíncrona.
  • Olvidar el contenido de repuesto: si se llama a until(promesa) con un único argumento, sin ningún valor de repuesto, la posición correspondiente de la plantilla queda simplemente vacía mientras la promesa está pendiente, lo cual puede resultar en una interfaz confusa (sin ningún indicio de que algo se está cargando) si no es intencionado.
  • Confundir la prioridad por posición del apartado 6 con la prioridad por orden de resolución: con varios argumentos, until no muestra siempre "lo último que ha resuelto", sino el argumento de mayor prioridad (más a la izquierda) que ya haya resuelto en ese momento; una promesa de menor prioridad que resuelve más tarde no sustituye a una de mayor prioridad que ya hubiera resuelto antes.

Ejercicios

  1. Modifica cargarTareas() para que, con una probabilidad del 20% (por ejemplo, Math.random() < 0.2), rechace la promesa en lugar de resolverla, simulando un fallo de red. Añade a _tareasTemplate un .catch() que devuelva una plantilla de error ("No se han podido cargar las tareas") en lugar de dejar que el rechazo se propague sin gestionar.
  2. Explica, basándote en el apartado 5, qué diferencia habría entre guardar this._tareasTemplate = cargarTareas().then(...) en el constructor (como en el apartado 4) y guardarlo en connectedCallback() en su lugar. ¿Sigue siendo correcto, o cambia algo relevante?
  3. Retoma la tabla del apartado 7 y decide, razonando tu respuesta, qué técnica usarías para una futura funcionalidad de TaskFlow que permita al usuario pulsar un botón "Guardar cambios" en <task-card>, mostrando un indicador mientras la operación esté en curso y permitiendo que el usuario pueda repetirla varias veces.

Soluciones

export function cargarTareas() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() < 0.2) {
        reject(new Error('Fallo simulado de red'));
        return;
      }
      resolve([
        { id: 't1', titulo: 'Diseñar la base de datos', estado: 'hecha', prioridad: 2 },
        { id: 't2', titulo: 'Implementar autenticación', estado: 'progreso', prioridad: 3 },
        { id: 't3', titulo: 'Escribir pruebas de integración', estado: 'pendiente', prioridad: 1 },
      ]);
    }, 1200);
  });
}
this._tareasTemplate = cargarTareas()
  .then((tareas) => {
    this.tareas = tareas;
    return html`<task-list .tareas="${tareas}" @tarea-cambiada="${this.gestionarTareaCambiada}"></task-list>`;
  })
  .catch(() => html`<p class="error">No se han podido cargar las tareas.</p>`);
  1. connectedCallback() puede llamarse más de una vez durante la vida de un componente, si el elemento se desconecta del DOM y se vuelve a conectar más adelante (por ejemplo, si se mueve de un contenedor a otro), tal como se explicó en la lección 06-01. Guardar this._tareasTemplate ahí, en lugar de en el constructor, arrancaría una nueva llamada a cargarTareas() cada vez que el componente se reconecte, exactamente el mismo problema señalado en el apartado 5 para render(), aunque con una frecuencia mucho menor. El constructor es preferible aquí precisamente porque se ejecuta garantizadamente una única vez en toda la vida de la instancia.
  2. Para "Guardar cambios" conviene ConEstadoCarga (o un patrón equivalente con una propiedad guardando propia de <task-card>), no until: la operación se repite cada vez que el usuario pulsa el botón, no una única vez en la vida del componente, y until no está pensado para sustituir su promesa por una nueva repetidamente de forma limpia. Con una propiedad de estado (guardando, alternada manualmente a true al iniciar la petición y a false al terminar, con éxito o con error), el botón puede deshabilitarse mientras la operación está en curso y volver a habilitarse al terminar, permitiendo tantos intentos como el usuario necesite, algo que encaja exactamente con el criterio de la fila "operación que se repite muchas veces" de la tabla del apartado 7.

Conclusión

Esta lección ha cerrado el problema del renderizado asíncrono con la directiva until, aplicada a la carga inicial de tareas de <task-board>: una promesa creada una única vez en el constructor, transformada con .then() en una promesa que resuelve directamente a una plantilla, y combinada con un esqueleto de carga como contenido de repuesto mientras la espera dura. La comparación con ConEstadoCarga ha dejado un criterio claro para el resto del curso: until para valores que se esperan una vez en una posición concreta de una plantilla, y una propiedad de estado explícita para operaciones que se repiten o que necesitan comunicar su estado más allá de esa única posición.

Con las tres piezas centrales de este módulo ya resueltas —directivas incorporadas, directivas personalizadas y renderizado asíncrono—, queda un último problema por resolver, distinto de todo lo anterior: <task-filter>, mencionado desde el módulo 5 pero nunca implementado, necesita comunicarse con <task-list> sin que ninguno de los dos conozca al otro directamente, y sin que <task-board> tenga que reenviar manualmente una propiedad entre ambos en cada cambio. La siguiente lección presenta @lit/context, la herramienta que TaskFlow necesitaba desde que se planteó ese problema por primera vez, y con ella, por fin, <task-filter> cobra vida.

Curso de Lit

Módulo 1: Introducción a Lit y Web Components

Módulo 2: Plantillas Reactivas y Renderizado

Módulo 3: Propiedades y Estado Reactivo

Módulo 4: Estilos en Componentes Lit

Módulo 5: Eventos y Comunicación entre Componentes

Módulo 6: Ciclo de Vida y Comportamiento Avanzado

Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas

Módulo 8: Integración, Interoperabilidad y Despliegue

Módulo 9: Pruebas y Buenas Prácticas

Módulo 10: Proyecto: Construyendo TaskFlow

© Copyright 2026. Todos los derechos reservados