The three previous lessons covered testing, accessibility, and performance, each as a cross-cutting quality angle applied to TaskFlow as it stood at the end of module 8. This last lesson of the module closes with a different kind of synthesis: instead of a new angle, it walks through the design decisions already made during the course from start to finish, groups them into pairs of recommended pattern versus anti-pattern, and explains why each anti-pattern, even though it sometimes "works" in the short term, ends up costing more than it saves. It is, in a sense, a review of the entire course seen from the other side: not what each piece of Lit does, but what specific mistake is avoided by using it well.
Contents
- Why a synthesis lesson is useful before the final project
- General table: patterns versus anti-patterns from the course
- Immutability versus mutating arrays and objects in place
composed: trueversus custom events that don't leave the Shadow DOM- Declarative templates versus business logic inside
render() - Decomposed components versus components that accumulate responsibilities
- Resource cleanup versus leaks in
disconnectedCallbackand controllers - A final criterion: questions that catch an anti-pattern before it's written
- Toward the final project: reviewing TaskFlow from start to finish
- Why a synthesis lesson is useful before the final project
Each anti-pattern in this lesson already appeared, at some point in the course, flagged as a "common mistake" at the end of a specific lesson, within the narrow context of the technique being explained at that moment. What that way of presenting them couldn't show is the pattern that repeats across different modules: that forgetting composed: true (module 5) and not cleaning up a setInterval in disconnectedCallback (module 6) are, at bottom, the same class of mistake —forgetting half of an operation that requires symmetry— even though they show up in two lessons separated by several modules. Seeing them together, now that the whole course is already known, makes them easier to recognize in new code, including the code that will be written in module 10.
- General table: patterns versus anti-patterns from the course
| # | Anti-pattern | Recommended pattern | Where it was explained |
|---|---|---|---|
| 1 | Mutating an array or object in place inside a reactive property | Replacing the property with a new copy (map, filter, {...obj}) |
"Parent-to-Child Communication with Properties" lesson (module 5) |
| 2 | Dispatching a custom event without composed: true from a component with Shadow DOM |
bubbles: true and composed: true together, so the event leaves the shadow root |
"Custom Events: Child-to-Parent Communication" lesson (module 5) |
| 3 | Putting business logic or expensive calculations directly inside render() |
Deriving that calculation in willUpdate, or extracting it to a separate method/controller |
"Reactive Hooks" (module 6) and "Performance and Optimization" (module 9) lessons |
| 4 | A single component that accumulates too many responsibilities without decomposing | Extracting sub-components with a clear responsibility, like <user-avatar> or <task-filter> |
"Slots and Styling Distributed Content" (module 4) and "Shared Context with @lit/context" (module 7) lessons |
| 5 | Not releasing resources (timers, subscriptions) in disconnectedCallback or in hostDisconnected |
Cleaning up in the same symmetric place where the resource was started | "Lifecycle Callbacks" and "Reactive Controllers" (module 6) lessons |
The five rows share the same underlying structure, even though at first glance they look like different problems: each anti-pattern "saves" a line or a step at the moment of writing it, and each recommended pattern invests that small upfront cost in exchange for predictable behavior later on, when the component grows, gets reused in a different context, or coexists with others it doesn't directly control.
- Immutability versus mutating arrays and objects in place
The "Parent-to-Child Communication with Properties" lesson explained, with the operations table from lesson 05-03, why this.tareas.push(...) or this.tareas[0].estado = 'hecha' don't trigger any visible update in Lit: the default comparison for a reactive property of object or array type is by reference, not by content, and an in-place mutation never changes that reference.
// Anti-pattern: mutates the existing array
gestionarTareaCambiada(idTarea, event) {
const tarea = this.tareas.find((t) => t.id === idTarea);
tarea.estado = event.detail.nuevoEstado;
this.requestUpdate(); // patch that hides the real problem
}
// Recommended pattern: replaces with a new copy
gestionarTareaCambiada(idTarea, event) {
const nuevoEstado = event.detail.nuevoEstado;
this.tareas = this.tareas.map((tarea) =>
tarea.id === idTarea ? { ...tarea, estado: nuevoEstado } : tarea
);
}The forced this.requestUpdate() in the first version isn't a fix, but a symptom: it "works" because it forces Lit to re-render regardless of whether it detected a real change, but it doesn't solve the underlying problem, and any other code relying on comparing the previous array with the current one (a debugging tool with history, or a future "undo" feature) would keep failing, because objects mutated in place leave no trace of what their previous value was. The immutable version needs no manual requestUpdate() because the assignment this.tareas = ... itself already changes the reference, which is exactly what Lit's reactivity system needs to detect on its own.
composed: true versus custom events that don't leave the Shadow DOM
composed: true versus custom events that don't leave the Shadow DOMThe "Custom Events: Child-to-Parent Communication" lesson flagged this mistake as the most frequent one when debugging communication between components, and it's worth repeating here alongside the rest because it shares the same nature as the previous section's anti-pattern: an operation that looks complete, but is missing an essential second half.
// Anti-pattern: the event never leaves <task-card>'s shadow root
this.dispatchEvent(new CustomEvent('tarea-cambiada', { detail: { nuevoEstado } }));
// Recommended pattern: bubbles and composed together
this.dispatchEvent(
new CustomEvent('tarea-cambiada', {
detail: { nuevoEstado },
bubbles: true,
composed: true,
})
);Without composed: true, the first version's code throws no error and no warning: the event is created, dispatched, and any console.log placed inside the method itself would misleadingly confirm that "everything works." The failure only shows up one step further, in <task-list>, which never receives the event because it stays trapped inside <task-card>'s shadow root, exactly the kind of silent failure —no error message, no console trace— that ends up more costly to diagnose than an explicit error.
- Declarative templates versus business logic inside
render()
render()The "Performance and Optimization" lesson (09-03) already explained the cost of recalculating inside render() something that could be derived once in willUpdate; this section revisits the same idea from a different angle, that of the code's own clarity, not just its cost:
// Anti-pattern: decides, calculates, and formats everything inside render()
render() {
const diasRestantes = Math.floor((this.fechaLimite - Date.now()) / 86400000);
const claseUrgencia = diasRestantes < 1 ? 'critico' : diasRestantes < 3 ? 'aviso' : 'normal';
return html`<p class="${claseUrgencia}">${diasRestantes} días restantes</p>`;
}
// Recommended pattern: render() only translates already-prepared data into HTML
willUpdate(changedProperties) {
if (changedProperties.has('fechaLimite')) {
this._diasRestantes = Math.floor((this.fechaLimite - Date.now()) / 86400000);
this._claseUrgencia = this._diasRestantes < 1 ? 'critico' : this._diasRestantes < 3 ? 'aviso' : 'normal';
}
}
render() {
return html`<p class="${this._claseUrgencia}">${this._diasRestantes} días restantes</p>`;
}Beyond the cost of recalculating on every render (already covered in the previous lesson), the first version mixes two different responsibilities inside a single method: deciding what the data means (how many days are left, and from how many days does something count as "critical"?) and deciding how to display it (which HTML tag, which CSS class). The second version splits both questions into two different places in the lifecycle, each with a clear responsibility: willUpdate decides the meaning, render() decides the representation. This separation, even though it looks like a simple reshuffling of the same code, makes it easier to reason about each half separately —and, not by coincidence, it's exactly the same separation already explained, with a different example, in the "Reactive Hooks" lesson from module 6.
- Decomposed components versus components that accumulate responsibilities
<task-card>, throughout the course, could have grown into a single monolithic class drawing directly, inside its own render(), both the assigned person's avatar and the whole application's search filter. Instead, the course has extracted, at the moment each responsibility became distinct enough, a component of its own: <user-avatar> in module 4, when showing an assigned person (with an image or initials, with its own <slot> and its own styling rules) stopped being a mere visual detail of <task-card> and became worthy of its own reusable class; <task-filter> in module 7, when managing the search filter stopped being something <task-list> could keep absorbing without mixing two unrelated responsibilities (showing tasks, on one hand; deciding which ones to show, on the other).
| Signal that a component is worth extracting | Example already applied in TaskFlow |
|---|---|
| A piece of the template has its own style lifecycle, independent of the rest | <user-avatar>, with its own --tamano-avatar CSS variable |
| A piece of behavior could be reused in a different context than the current one | <user-avatar>, designed to be able to appear anywhere in TaskFlow that needs to show a person, not just inside <task-card> |
| Two responsibilities within the same class change for different reasons and at different times | <task-filter> changes when filtering rules change; <task-list> changes when how tasks are displayed changes |
| A class name starts needing the conjunction "and" to describe itself ("the list that also filters and also paginates") | Avoided by extracting <task-filter> instead of absorbing the filter inside <task-list> |
The symmetric anti-pattern to avoid is the opposite one: extracting a new component for every tiny detail, with none of the signals from the table above present, multiplying the number of files and Shadow DOMs with no real gain in clarity or reuse. The criterion, as with the rest of this lesson, isn't "always decompose" nor "never decompose," but recognizing the specific signals that justify the extraction, exactly as TaskFlow has done in both cases in the table.
- Resource cleanup versus leaks in
disconnectedCallback and controllers
disconnectedCallback and controllersThe last pair in this lesson directly connects modules 6 and 9: the "Lifecycle Callbacks" lesson warned about not cleaning up a setInterval in disconnectedCallback, and the "Reactive Controllers" lesson repeated the same warning, this time about hostDisconnected, pointing out that the risk multiplies with every host using the controller.
// Anti-pattern: starts the timer, never stops it
connectedCallback() {
super.connectedCallback();
this._idIntervalo = setInterval(() => this._comprobar(), 60000);
}
// (no disconnectedCallback, or one that doesn't call clearInterval)
// Recommended pattern: symmetry between connection and disconnection
connectedCallback() {
super.connectedCallback();
this._idIntervalo = setInterval(() => this._comprobar(), 60000);
}
disconnectedCallback() {
clearInterval(this._idIntervalo);
super.disconnectedCallback();
}A <task-card> removed from the DOM (for example, because <task-filter>'s filter excludes it from the result) without its timer being stopped keeps running _comprobar() every minute, indefinitely, even though there's no longer any visible node that check could affect; with hundreds of cards created and destroyed over a long usage session (the large-list scenario explored in the previous lesson), those orphaned timers pile up one after another, consuming memory and processing cycles with no benefit, a problem that only shows up over time and rarely appears in a quick five-minute manual test, precisely the kind of failure that an automated test suite like the one in lesson 09-01 also fails to catch unless specifically designed to check for it.
- A final criterion: questions that catch an anti-pattern before it's written
The five pairs above share a common trait worth turning into a habit, not just a list to consult after writing the code: each anti-pattern corresponds to a specific question that, asked in time, would have prevented it.
| Question worth asking | Anti-pattern it catches |
|---|---|
| Am I replacing this property with a new value, or modifying the value it already had? | In-place mutation (section 3) |
| If this component has Shadow DOM, does this event need to leave it? | Missing composed: true (section 4) |
| Does this calculation depend only on properties that have already changed, or does it repeat unconditionally? | Business logic in render() (section 5) |
| Does this class change for more than one distinct reason? | Undecomposed responsibilities (section 6) |
| For every resource I start, where exactly am I going to stop it? | Resource leak (section 7) |
- Toward the final project: reviewing TaskFlow from start to finish
With this synthesis, module 9 completes the quality coverage TaskFlow needed before it could be considered a finished application: automated tests that verify its behavior without relying on manual checks, accessibility for those who don't use a mouse on a conventional screen, performance reviewed with judgment against realistic data volumes, and now a catalog of patterns and anti-patterns that sums up, at a glance, the design decisions from the eight previous modules.
What remains isn't any new Lit concept —the course has already covered templates, reactivity, styles, events, lifecycle, directives, context, integration, testing, and best practices— but walking through TaskFlow from start to finish as a complete application: reviewing each component built throughout the course, identifying which pieces were mentioned but not fully resolved, and finally assembling them into the finished, final version of the application. That is, precisely, the task of the course's very last lesson, "Project: Building TaskFlow," the closing module that wraps up the full journey from the first Lit component in lesson 01-03 to the final application.
Common Mistakes and Tips
- Treating this lesson as a list of isolated rules, instead of a transferable criterion: the five pairs in section 2 aren't a closed list of TaskFlow-specific mistakes, but concrete examples of a handful of general questions (section 8) that can be applied to any future Lit component, including any added in the final project.
- Applying a recommended pattern without understanding which anti-pattern it avoids: copying
bubbles: true, composed: trueonto every event "out of habit," without knowing it solves the specific problem of crossing the Shadow DOM, leaves whoever writes the code without the judgment to decide when, on rare occasions, neither option would be needed (an event a component only ever needs to listen to on itself, with no interested parent). - Confusing "decomposing into components" with a numeric rule: as noted in section 6, there's no correct number of components per application; the signals from section 6's table, not an arbitrary count, are the criterion that should decide when to extract a new piece.
- Reviewing anti-patterns only at the end of a project, not during its development: this lesson deliberately comes before the final project, not after; the goal is for the questions in section 8 to become a habit while building module 10, not a checklist rushed through once everything is finished.
Exercises
- Review
<task-board>'srender()as it stood after the "Mixins and Behavior Composition" lesson from module 6 (withConEstadoCarga) and point out, using this lesson's section 5 criterion, whetherthis.conIndicadorDeCarga(...)counts as "business logic insiderender()" or fits the recommended pattern better. Justify the answer. - A teammate, reviewing
<task-filter>, finds thatmanejarTextoandmanejarEstadocallthis.valorActual.actualizar({...}), which in turn reassignsthis._filtroProvider.valuein<task-board>using the spread operator ({ ...this._filtroProvider.value, ...cambios }, seen in the "Shared Context with @lit/context" lesson). Explain, using section 3's criterion, why that reassignment counts as the recommended pattern and not the in-place mutation anti-pattern. - Applying the five questions from section 8, identify which one would have flagged the problem if
ContadorTiempoRestanteController(module 6) had directly assignedthis.host.cercaDeVencer = ...instead of keeping its owncercaDeVencerfield and callingthis.host.requestUpdate(), as warned in the "Common Mistakes" section of the "Reactive Controllers" lesson.
Solutions
this.conIndicadorDeCarga(...)is not business logic insiderender()in the sense section 5 flags: it doesn't calculate or derive any new data from properties (it doesn't decide "whatcargandomeans"; that value already arrives computed as a simple boolean property); it merely translates an already-existing value (this.cargando) into one of two alternative templates, exactly the responsibility section 5 reserves forrender(). IfconIndicadorDeCargainstead calculated something like "how long it's been loading" from a timestamp, that calculation would indeed fit better inwillUpdate, but that's not what it does in its current form.- The reassignment
this._filtroProvider.value = { ...this._filtroProvider.value, ...cambios }follows exactly the recommended pattern from section 3: instead of modifying the existing context object by adding or changing a property in place (this._filtroProvider.value.texto = cambios.texto, which would mutate the object without changing its reference), it creates a completely new object via the spread operator, combining the previous value with the received changes, and assigns that new object to.value. That assignment changes the context value's reference, exactly whatContextProviderneeds in order to correctly notify subscribed consumers, the same reference-based detection mechanism explained for regular reactive properties in section 3. - The question that would have flagged it is the first one: "Am I replacing this property with a new value, or modifying the value it already had?", though applied here not to an array mutation, but to the relationship between the controller and its host. Directly assigning
this.host.cercaDeVencer = ...from inside the controller couples the controller's internal mechanism to a specific reactive property the host would have to declare just to accommodate that implementation detail, exactly the same kind of unnecessary coupling section 6 warns against between responsibilities that should stay separate: the controller decides its own result; the host, throughrequestUpdate(), only learns that it needs to re-render, without both responsibilities getting mixed into a single shared property.
Conclusion
This lesson has reviewed, in five pairs, the most frequently flagged anti-patterns throughout the course against the recommended alternative TaskFlow has applied in each case: immutability instead of in-place mutation, composed: true so events cross the Shadow DOM, derived calculation kept outside render(), components decomposed based on specific signals, and symmetric cleanup of any resource started in the lifecycle. With this review, and with the testing, accessibility, and performance from the three previous lessons, module 9 completes TaskFlow's quality coverage.
This also closes the course's theoretical content as a whole. A single lesson remains, the course's last: "Project: Building TaskFlow," where no new concept is introduced, but the application is walked through from start to finish, the pieces mentioned but never fully closed off throughout the course are reviewed and completed, and TaskFlow is delivered as the finished application that crowns the entire journey, from the first Lit component to the final project.
Lit Course
Module 1: Introduction to Lit and Web Components
- What are Web Components and why Lit?
- Setting Up the Development Environment
- Your First Lit Component
- Anatomy of a Lit Component
Module 2: Reactive Templates and Rendering
- Lit's Template Engine
- Expressions and Interpolation in Templates
- Conditional Rendering
- List Rendering
- The Rendering Cycle
Module 3: Reactive Properties and State
- Reactive Properties
- Internal State with @state
- Types of Properties and Custom Converters
- Attributes vs Properties and Reflection
Module 4: Styling Lit Components
- Encapsulated CSS with Shadow DOM
- Shared Styles Between Components
- Custom CSS Properties and Theming
- Slots and Styling Distributed Content
Module 5: Events and Component Communication
- Handling DOM Events in Templates
- Custom Events: Communication from Child to Parent
- Communication from Parent to Child with Properties
- Communication Patterns Between Sibling Components
Module 6: Lifecycle and Advanced Behavior
- Lifecycle Callbacks
- Reactive Hooks: willUpdate, updated, and firstUpdated
- Reactive Controllers
- Mixins and Composing Behavior
Module 7: Directives and Advanced Template Features
- Built-in Directives: classMap, styleMap and ifDefined
- Custom Directives
- Asynchronous Rendering with until
- Shared Context with @lit/context
Module 8: Integration, Interoperability and Deployment
- Using Lit Components in Plain HTML
- Integrating Lit with React, Vue, and Angular
- Server-Side Rendering with @lit-labs/ssr
- Bundling, Publishing, and TypeScript
Module 9: Testing and Best Practices
- Unit Tests with Web Test Runner
- Accessibility in Web Components
- Performance and Optimization
- Common Patterns and Anti-patterns
