Fins ara, TaskFlow ha treballat sempre amb dades ja disponibles per endavant: l'array tareas de <task-board> s'ha inicialitzat directament al constructor, amb valors d'exemple escrits a mà, llestos per renderitzar-se des del primer moment. Cap projecte real funciona així: les tasques d'una aplicació de gestió arribarien d'una API, d'una base de dades local, o de qualsevol altra font que tarda un temps —normalment indeterminat— a respondre. Aquesta lliçó resol aquest problema amb la directiva until, i aprofita per connectar aquesta solució amb el mixin ConEstadoCarga del mòdul 6, que ja apuntava en la mateixa direcció amb una eina distinta.
Contingut
- El problema: mostrar dades que encara no han arribat
- La directiva
untili la seua signatura bàsica - Simulant una càrrega real:
cargarTareas() - Aplicant
untila<task-board>amb un esquelet de càrrega - Un error habitual: crear la promesa dins de
render() untilamb diversos valors: prioritat per posicióuntildavantConEstadoCarga: quan convé cadascun
- El problema: mostrar dades que encara no han arribat
render(), tal com s'ha estudiat des del mòdul 2, sempre ha d'executar-se de forma síncrona: rep l'estat actual del component i retorna immediatament una plantilla, sense esperar res. Aquesta restricció no és negociable ni té cap via d'escapament especial: render() mai pot ser una funció async, ni pot retornar directament una Promise perquè Lit "espere" que es resolga abans de pintar alguna cosa en pantalla.
El problema, doncs, és evident: si <task-board> necessitara carregar les seues tasques des d'una funció asíncrona (cargarTareas(), que tarda un temps a resoldre's), què hauria de retornar render() mentre aquella promesa encara està pendent? Sense cap eina addicional, l'única opció seria recórrer a alguna variant del que ja s'ha vist al mòdul 6: guardar el resultat en una propietat d'estat (this.tareas), inicialitzar-la buida, i actualitzar-la quan la promesa es resolga, deixant que el nou valor dispare un nou render() pel mecanisme habitual de propietats reactives. Aquesta solució funciona, i de fet és la que s'ha usat implícitament fins ara sense anomenar-la; el mixin ConEstadoCarga de la lliçó 06-04 afegia, sobre aquella mateixa base, una propietat cargando explícita per mostrar un avís mentre l'espera dura. Aquesta lliçó presenta una alternativa més declarativa, pensada específicament per a aquest problema: expressar l'espera directament dins de la plantilla, sense necessitat d'una propietat d'estat intermèdia dedicada només a saber si alguna cosa continua carregant.
- La directiva
until i la seua signatura bàsica
until i la seua signatura bàsicauntil rep, en el seu ús més simple, dos arguments: una Promise i un valor de reserva (normalment una altra plantilla html, encara que pot ser qualsevol valor renderitzable). Mentre la promesa no s'haja resolt, until mostra el contingut de reserva; en l'instant en què la promesa es resol, until substitueix automàticament aquell contingut pel valor resolt, sense que el component que l'usa haja de gestionar manualment cap propietat d'estat per saber en quin moment passa aquella substitució. Per dins, until no és més que una altra directiva personalitzada, construïda exactament amb les mateixes peces —directive() i Directive— que s'han presentat a la lliçó anterior: manté el seu propi estat intern (quin valor mostrar en cada moment) i usa part per actualitzar la posició del DOM quan la promesa es resol, sense que el seu usuari necessite conèixer res d'aquell detall intern per aprofitar-la.
- Simulant una càrrega real:
cargarTareas()
cargarTareas()Per poder aplicar until a un cas real de TaskFlow, cal primer alguna cosa que simule l'espera d'una font de dades externa. Sense entrar encara en peticions de xarxa reals (això correspon al mòdul 8), n'hi ha prou amb una funció que retorne una Promise que es resol després d'un breu retard simulat amb 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);
});
}Aquesta funció no forma part del mateix component <task-board>; viu en un mòdul de servei separat, seguint la mateixa idea de separació de responsabilitats que ja ha aparegut en altres punts del curs (els controladors del mòdul 6, per exemple): <task-board> no necessita saber com s'obtenen les tasques, només que cargarTareas() retorna una promesa que eventualment es resol amb un array de tasques.
- Aplicant
until a <task-board> amb un esquelet de càrrega
until a <task-board> amb un esquelet de càrregaAmb la simulació ja llesta, <task-board> guarda la promesa retornada per cargarTareas() una única vegada, i la transforma amb .then() en una promesa que resol directament a una plantilla, llesta per passar-se 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 punt central d'aquest codi és this._tareasTemplate, construïda al constructor amb cargarTareas().then((tareas) => { ... }): en lloc de guardar la promesa "en brut" de cargarTareas() (que resoldria amb un array de dades), s'encadena un .then() que fa dues coses a la vegada —actualitzar this.tareas com a efecte secundari, per si algun altre punt del component necessitara llegir aquell array directament, i retornar ja una plantilla html amb <task-list> completament muntada—. El resultat, this._tareasTemplate, és una promesa que resol directament a alguna cosa renderitzable, exactament el que until espera com a primer argument.
A render(), until(this._tareasTemplate, this.renderEsqueleto()) mostra l'esquelet de càrrega (tres barres amb una animació de pols, definida a static styles) mentre la promesa continue pendent, i el substitueix automàticament per <task-list> en l'instant en que cargarTareas() es resol, sense que <task-board> haja hagut de declarar cap propietat cargando per a aquesta operació en concret.
- Un error habitual: crear la promesa dins de
render()
render()El detall més important de tot l'exemple anterior, fàcil de passar per alt, és que this._tareasTemplate es crea una única vegada, al constructor, no dins de render(). Escriure això en el seu lloc seria un error greu:
// Incorrecte: no facis això
render() {
return html`
<div class="tablero">
<h1>TaskFlow</h1>
${until(cargarTareas().then((tareas) => html`<task-list .tareas="${tareas}">...</task-list>`), this.renderEsqueleto())}
</div>
`;
}render() pot executar-se moltes vegades durant la vida d'un component, cada vegada que canvia qualsevol propietat reactiva (com s'ha explicat a la lliçó 02-05). Si cargarTareas() es cridara dins de render(), cada nova execució de render() —fins i tot una provocada per un canvi completament aliè a la càrrega de tasques— dispararia una nova crida a la funció, iniciant una nova simulació de xarxa des de zero i mostrant de nou l'esquelet de càrrega mentre aquella nova promesa estiga pendent, en un bucle de destellaments que mai arribaria a estabilitzar-se del tot. Per això this._tareasTemplate es calcula una única vegada, al constructor (el lloc adequat per a qualsevol operació que haja d'ocórrer exactament una vegada en la vida del component, com ja s'ha vist amb els controladors reactius i els mixins al mòdul 6), i render() es limita a llegir aquella mateixa referència, ja estable, en cada execució.
until amb diversos valors: prioritat per posició
until amb diversos valors: prioritat per posicióuntil admet, a més de la forma de dos arguments ja vista, qualsevol nombre de valors, no necessàriament tots promeses:
Amb diversos arguments, until els tracta amb un ordre de prioritat estricte segons la seua posició, no segons l'ordre en què resolen: mentre cap haja resolt encara, es mostra l'últim argument (el de menor prioritat, normalment un valor síncron, no una promesa); en el moment en que resol qualsevol dels arguments anteriors, es mostra el seu valor, però en el moment en que resol un de posició més alta (més cap a l'esquerra), until canvia a mostrar aquell en el seu lloc, i ja no torna als de menor prioritat encara que aquests actualitzen el seu valor més endavant. Això permet construir una càrrega progressiva amb més d'un nivell de detall —per exemple, un resum ràpid calculat en el mateix navegador mentre s'espera la resposta completa d'una font més lenta—, cosa que TaskFlow no necessita encara amb una única font de dades com cargarTareas(), però que convé conèixer per reconèixer el patró si apareix en codi de tercers o en necessitats futures del projecte.
until davant ConEstadoCarga: quan convé cadascun
until davant ConEstadoCarga: quan convé cadascunAmb les dues eines ja disponibles per al mateix problema general —mostrar alguna cosa mentre s'espera una operació asíncrona—, convé fixar el criteri per triar entre elles en cada situació futura de TaskFlow:
| Criteri | until |
ConEstadoCarga (mixin, lliçó 06-04) |
|---|---|---|
| On viu la lògica d'espera | Declarada directament a la plantilla, en el punt exacte on cal | En una propietat cargando del mateix component, gestionada explícitament |
| L'estat de càrrega és visible fora d'aquella posició de la plantilla? | No, de forma directa; només afecta aquella expressió concreta | Sí: this.cargando es pot llegir i usar en qualsevol part del component (per exemple, per deshabilitar un botó) |
| Encaixa bé amb un valor que arriba una sola vegada? | Sí, és el seu cas d'ús principal | També, però requereix gestionar manualment el canvi de true a false |
| Encaixa bé amb una operació que es repeteix moltes vegades (guardar, reintentar)? | Pitjor: cada nova operació exigeix una nova promesa i, amb cura, tornar a mostrar un estat de "pendent" | Millor: n'hi ha prou amb alternar this.cargando entre true i false tantes vegades com calga |
| Exemple d'aquest curs | Càrrega inicial de tasques a <task-board>, un únic valor esperat una vegada |
Qualsevol operació futura que necessite comunicar el seu estat de càrrega a diverses parts del component, o repetir-se diverses vegades |
El criteri de fons és que until és una solució declarativa, lligada a una única posició de la plantilla i a una única promesa concreta, ideal per al cas d'aquesta lliçó: un valor que s'espera una vegada i que, en el moment en que arriba, substitueix netament el seu contingut de reserva. ConEstadoCarga, en canvi, exposa el seu estat com una propietat normal del component, més flexible per a operacions que es repeteixen en el temps o que necessiten comunicar el seu estat de càrrega més enllà d'una única posició d'una única plantilla. Totes dues tècniques són complementàries, no mútuament excloents: <task-board> continua estenent ConEstadoCarga(LitElement) en l'exemple de l'apartat 4, disponible per a qualsevol futura operació de TaskFlow (com guardar canvis) que sí encaixe millor amb aquest patró, mentre que la càrrega inicial de tasques, en ser una operació d'una sola vegada, usa until en el seu lloc.
Errors Comuns i Consells
- Crear la promesa dins de
render()en lloc delconstructor: com s'ha explicat a l'apartat 5, això reinicia l'operació asíncrona en cada renderitzat, provocant destellaments repetits del contingut de reserva i, en un cas real amb una petició de xarxa, peticions duplicades innecessàries. - Esperar que
untilfuncione amb una funcióasynccomrender():render()mai pot declarar-seasyncni retornar unaPromisedirectament;untilés precisament el mecanisme que permet mantenirrender()síncron mentre es representa, dins d'una posició concreta de la plantilla, el resultat d'una operació que sí és asíncrona. - Oblidar el contingut de reserva: si es crida
until(promesa)amb un únic argument, sense cap valor de reserva, la posició corresponent de la plantilla queda simplement buida mentre la promesa està pendent, cosa que pot resultar en una interfície confusa (sense cap indici de que alguna cosa s'està carregant) si no és intencionat. - Confondre la prioritat per posició de l'apartat 6 amb la prioritat per ordre de resolució: amb diversos arguments,
untilno mostra sempre "el que ha resolt més recentment", sinó l'argument de major prioritat (més cap a l'esquerra) que ja haja resolt en aquell moment; una promesa de menor prioritat que resol més tard no substitueix una de major prioritat que ja hi haguera resolt abans.
Exercicis
- Modifica
cargarTareas()perquè, amb una probabilitat del 20% (per exemple,Math.random() < 0.2), rebutge la promesa en lloc de resoldre-la, simulant una fallada de xarxa. Afig a_tareasTemplateun.catch()que retorne una plantilla d'error ("No se han podido cargar las tareas") en lloc de deixar que el rebuig es propague sense gestionar. - Explica, basant-te en l'apartat 5, quina diferència hi hauria entre guardar
this._tareasTemplate = cargarTareas().then(...)alconstructor(com a l'apartat 4) i guardar-lo aconnectedCallback()en el seu lloc. Continua sent correcte, o canvia alguna cosa rellevant? - Reprén la taula de l'apartat 7 i decideix, raonant la teua resposta, quina tècnica usaries per a una futura funcionalitat de TaskFlow que permeta a l'usuari prémer un botó "Guardar cambios" a
<task-card>, mostrant un indicador mentre l'operació estiga en curs i permetent que l'usuari puga repetir-la diverses vegades.
Solucions
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>`);connectedCallback()pot cridar-se més d'una vegada durant la vida d'un component, si l'element es desconnecta del DOM i es torna a connectar més endavant (per exemple, si es mou d'un contenidor a un altre), tal com s'ha explicat a la lliçó 06-01. Guardarthis._tareasTemplateallí, en lloc delconstructor, arrancaria una nova crida acargarTareas()cada vegada que el component es reconnecte, exactament el mateix problema assenyalat a l'apartat 5 per arender(), encara que amb una freqüència molt menor. Elconstructorés preferible ací precisament perquè s'executa garantidament una única vegada en tota la vida de la instància.- Per a "Guardar cambios" convé
ConEstadoCarga(o un patró equivalent amb una propietatguardandopròpia de<task-card>), nountil: l'operació es repeteix cada vegada que l'usuari prem el botó, no una única vegada en la vida del component, iuntilno està pensat per substituir la seua promesa per una nova repetidament de forma neta. Amb una propietat d'estat (guardando, alternada manualment atrueen iniciar la petició i afalseen acabar, amb èxit o amb error), el botó es pot deshabilitar mentre l'operació està en curs i tornar a habilitar-se en acabar, permetent tants intents com l'usuari necessite, cosa que encaixa exactament amb el criteri de la fila "operació que es repeteix moltes vegades" de la taula de l'apartat 7.
Conclusió
Aquesta lliçó ha tancat el problema del renderitzat asíncron amb la directiva until, aplicada a la càrrega inicial de tasques de <task-board>: una promesa creada una única vegada al constructor, transformada amb .then() en una promesa que resol directament a una plantilla, i combinada amb un esquelet de càrrega com a contingut de reserva mentre l'espera dura. La comparació amb ConEstadoCarga ha deixat un criteri clar per a la resta del curs: until per a valors que s'esperen una vegada en una posició concreta d'una plantilla, i una propietat d'estat explícita per a operacions que es repeteixen o que necessiten comunicar el seu estat més enllà d'aquella única posició.
Amb les tres peces centrals d'aquest mòdul ja resoltes —directives incorporades, directives personalitzades i renderitzat asíncron—, queda un últim problema per resoldre, distint de tot l'anterior: <task-filter>, mencionat des del mòdul 5 però mai implementat, necessita comunicar-se amb <task-list> sense que cap dels dos conega l'altre directament, i sense que <task-board> haja de reenviar manualment una propietat entre ambdós en cada canvi. La lliçó següent presenta @lit/context, l'eina que TaskFlow necessitava des que es va plantejar aquell problema per primera vegada, i amb ella, per fi, <task-filter> pren vida.
Curs de Lit
Mòdul 1: Introducció a Lit i Web Components
- Què són els Web Components i per què Lit?
- Configuració de l'Entorn de Desenvolupament
- El Teu Primer Component Lit
- Anatomia d'un Component Lit
Mòdul 2: Plantilles Reactives i Renderitzat
- El Motor de Plantilles de Lit
- Expressions i Interpolació en Plantilles
- Renderitzat Condicional
- Renderitzat de Llistes
- El Cicle de Renderitzat
Mòdul 3: Propietats i Estat Reactiu
- Propietats Reactives
- Estat Intern amb @state
- Tipus de Propietats i Conversors Personalitzats
- Atributs vs Propietats i Reflexió
Mòdul 4: Estils en Components Lit
- CSS Encapsulat amb Shadow DOM
- Estils Compartits entre Components
- Variables CSS Personalitzades i Theming
- Slots i Estilitzat de Contingut Distribuït
Mòdul 5: Esdeveniments i Comunicació entre Components
- Gestió d'Esdeveniments DOM en Plantilles
- Esdeveniments Personalitzats: Comunicació de Fill a Pare
- Comunicació de Pare a Fill amb Propietats
- Patrons de Comunicació entre Components Germans
Mòdul 6: Cicle de Vida i Comportament Avançat
- Callbacks del Cicle de Vida
- Hooks Reactius: willUpdate, updated i firstUpdated
- Controladors Reactius
- Mixins i Composició de Comportament
Mòdul 7: Directives i Funcionalitats Avançades de Plantilles
- Directives Incorporades: classMap, styleMap i ifDefined
- Directives Personalitzades
- Renderitzat Asíncron amb until
- Context Compartit amb @lit/context
Mòdul 8: Integració, Interoperabilitat i Desplegament
- Utilitzar Components Lit en HTML Pla
- Integrar Lit amb React, Vue i Angular
- Renderitzat al Servidor amb @lit-labs/ssr
- Empaquetatge, Publicació i TypeScript
Mòdul 9: Proves i Bones Pràctiques
- Proves Unitàries amb Web Test Runner
- Accessibilitat en Web Components
- Rendiment i Optimització
- Patrons i Antipatrons Comuns
