Guía de respuestas y soluciones razonadas: Módulo 03 — Frontend y Navegador#
Esta guía de referencia técnica presenta las soluciones canónicas, las implementaciones de referencia en HTML/CSS accesible, TypeScript seguro en runtime, componentes de estado mínimo en React, e integración de extremo a extremo con APIs HTTP, junto con las respuestas exhaustivas a las preguntas de diagnóstico socrático para las lecciones 09 a 12 del módulo de frontend.
Intenta resolver primero cada ejercicio práctico, auditoría de accesibilidad y trazabilidad concurrente por tu cuenta antes de consultar esta guía. El aprendizaje profundo en ingeniería de interfaces y cliente web proviene de operar con el teclado real, depurar la pestaña de red y razonar sobre la frontera entre los tipos estáticos del compilador y la ejecución dinámica del navegador.
Para consultar definiciones formales y etimología de cualquier término técnico, visita el glosario técnico.
Lección 09 — HTML, CSS y accesibilidad#
1. Implementación canónica de referencia: Formulario semántico y accesible#
El siguiente documento es un archivo HTML5 completo y autónomo, con estilos CSS integrados que cumplen los criterios de conformidad WCAG 2.1 Nivel AA para contraste, foco visible y diseño adaptativo:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Task Creation · Semantic and Accessible Form</title>
<style>
/* 1. Global Box-Sizing Reset */
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 2rem 1rem;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.5;
color: #1a1a1a;
background-color: #f8f9fa;
}
/* 2. Structured Layout using CSS Grid */
main {
max-width: 34rem;
margin: 0 auto;
padding: 2.5rem 2rem;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
h1 {
margin-top: 0;
margin-bottom: 1.75rem;
font-size: 1.5rem;
color: #111827;
}
form {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.field-group {
display: grid;
grid-template-columns: 1fr;
gap: 0.35rem;
}
label {
font-weight: 600;
font-size: 0.95rem;
color: #1f2937;
}
.field-hint {
margin: 0;
font-size: 0.85rem;
color: #4b5563; /* WCAG AA > 4.5:1 contrast against white */
}
input[type="text"],
input[type="date"] {
width: 100%;
padding: 0.75rem;
font-size: 1rem;
border: 1px solid #6b7280; /* > 3:1 graphical boundary contrast */
border-radius: 4px;
background-color: #ffffff;
color: #111827;
}
/* 3. Highly Visible Accessible Focus */
input:focus-visible,
button:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
}
/* 4. Multi-modal Error Presentation (Not relying solely on color) */
input[aria-invalid="true"] {
border: 2px solid #b91c1c;
}
.field-error {
margin: 0;
font-size: 0.85rem;
font-weight: 600;
color: #b91c1c; /* Contrast ratio 5.6:1 against white */
display: flex;
align-items: center;
gap: 0.25rem;
}
/* 5. Native Accessible Button */
button[type="submit"] {
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
color: #ffffff;
background-color: #0f766e;
border: none;
border-radius: 4px;
cursor: pointer;
justify-self: start;
transition: background-color 0.15s ease-in-out;
}
button[type="submit"]:hover {
background-color: #115e59;
}
</style>
</head>
<body>
<main>
<h1>Create a new task</h1>
<form action="/api/tasks" method="post" novalidate>
<!-- Field 1: Title (Mandatory) -->
<div class="field-group">
<label for="task-title">Title (required)</label>
<input
id="task-title"
name="title"
type="text"
required
aria-required="true"
aria-invalid="true"
aria-describedby="title-help title-error"
>
<p id="title-help" class="field-hint">Enter a brief summary of the task to be completed.</p>
<p id="title-error" class="field-error" role="alert">
<span aria-hidden="true">⚠️</span> Error: Title cannot be empty or contain only whitespace.
</p>
</div>
<!-- Field 2: Due Date (Optional) -->
<div class="field-group">
<label for="task-due-date">Due date (optional)</label>
<input
id="task-due-date"
name="due_date"
type="date"
aria-describedby="due-date-help"
>
<p id="due-date-help" class="field-hint">Select a deadline date for completion.</p>
</div>
<!-- Submit Button -->
<button type="submit">Save task</button>
</form>
</main>
</body>
</html>
2. Desglose matemático exacto del modelo de caja (Box Model)#
Cálculo con box-sizing: content-box (Predeterminado del navegador):#
Supongamos un contenedor padre con ancho útil de 500 px. El elemento hijo tiene:
width: 100%arrow 500 px aplicados exclusivamente al área de contenido interior.padding: 16pxa izquierda y derecha arrow 16 + 16 = 32 px.border: 2pxa izquierda y derecha arrow 2 + 2 = 4 px.
Resultado geométrico:
El elemento sobresale 36 píxeles fuera de su contenedor. En pantallas móviles esto produce una barra de desplazamiento horizontal no deseada (horizontal scrollbar), arruinando la cuadrícula y cortando los bordes.
Cálculo con box-sizing: border-box (Reseteo universal):#
El elemento hijo tiene las mismas reglas, pero bajo border-box:
width: 100%arrow 500 px aplicados a la totalidad de la caja (desde el borde exterior izquierdo hasta el derecho).- El padding (32 px) y el borde (4 px) se absorben hacia el interior.
- El área de contenido disponible para texto se encoge automáticamente:
Resultado geométrico:
Ajuste perfecto sin desbordamiento ni parches artificiales.
3. Matriz de verificación de auditoría de accesibilidad y teclado#
| # | Punto de auditoría | Herramienta / Método | Comportamiento verificado en la solución |
|---|---|---|---|
| 1 | Secuencia de tabulación lógica | Teclado (Tab, Shift + Tab) | El foco recorre ordenadamente: Campo Título arrow Campo Fecha arrow Botón Guardar. Sin saltos erráticos ni trampas de foco. |
| 2 | Foco visible de alto contraste | Inspección visual con teclado | :focus-visible dibuja un anillo azul continuo de 3px con desplazamiento de 2px (outline-offset: 2px). Visible sobre fondos claros y oscuros. |
| 3 | Activación nativa dual | Teclas Enter y Space | El botón Guardar se acciona tanto con Enter como con Space. Presionar Space no causa scroll en la página. |
| 4 | Inspección en el Árbol de Accesibilidad (AOM) | DevTools arrow Elements arrow Accessibility | El <input> expone: Role: textbox, Name: "Title (required)", Invalid: true, Description: "Enter a brief summary... Error: Title cannot be empty.". |
| 5 | Superficie táctil de la etiqueta | Ratón / Puntero táctil | Hacer clic sobre el texto de la etiqueta transfiere el foco y activa el cursor dentro del <input> respectivo gracias a for/id. |
| 6 | Independencia del color (WCAG 1.4.1) | Simulación de acromatopsia en DevTools | El error es perfectamente comprensible sin color gracias al texto explícito "Error:...", el icono de advertencia ⚠️ y el grosor duplicado del borde (2px). |
| 7 | Contraste de color (WCAG 1.4.3) | Cuentagotas de contraste DevTools | Texto principal: 15.3:1 (supera con creces el mínimo de 4.5:1). Texto de error: 5.6:1 (AA cumplido). |
4. Respuestas a las preguntas de diagnóstico socrático (Lección 09)#
Pregunta 1: La trampa del botón estilizado con <div>#
- Reporte en el Árbol de Accesibilidad (AOM): El navegador expone un nodo genérico neutro (
Role: genericotext). No reporta acciones disponibles (Actions: []), no expone estados accionables ni comunica al sintetizador de voz que se trata de un control interactivo. - Exclusión de la secuencia de tabulación: En el modelo de objetos de HTML, los únicos elementos interactivos enfocables por defecto son aquellos con semántica nativa interactiva (
<a>conhref,<button>,<input>,<select>,<textarea>). Un<div>carece de foco nativo; el usuario de teclado lo saltará por completo. - El problema de
tabindex="0"y la barra espaciadora: Aunquetabindex="0"permite que el foco llegue al<div>, la especificación de teclado de los navegadores asigna a la barra espaciadora la acción nativa de desplazarse hacia abajo en la página (page scroll). En un botón nativo, el navegador intercepta la barra espaciadora enkeydowny previene el scroll automáticamente. En un<div>, presionar la barra espaciadora disparará el scroll de la página y desorientará completamente al usuario, a menos que se escriba un escuchador de teclado que invoque manualmenteevent.preventDefault().
Pregunta 2: El colapso del placeholder como única etiqueta#
- Impacto en la memoria de trabajo y cognición: En cuanto el usuario empieza a escribir, el texto del placeholder se borra. Si el usuario se distrae, atiende una llamada o tiene problemas de memoria a corto plazo, la pantalla ya no muestra qué dato se estaba solicitando. El usuario se ve obligado a borrar lo que escribió solo para recordar qué pedía el campo.
- Pérdida de contexto en la corrección: Si el navegador autocompleta un valor erróneo o el usuario comete una equivocación, no hay forma visual persistente de contrastar el dato contra los requerimientos originales.
- Fallo en lectores de pantalla: La especificación de accesibilidad del W3C define el algoritmo de cálculo de nombre accesible dando prioridad a etiquetas asociadas (
<label>,aria-labelledby,aria-label). Muchos lectores de pantalla degradan el placeholder o lo anuncian de forma confusa como valor editable predeterminado en lugar de como etiqueta formal de identificación.
Pregunta 3: Diagnóstico geométrico de desbordamiento horizontal#
- Por qué
overflow-x: hiddenes un parche cosmético perjudicial: Ocultar el desbordamiento simplemente recorta los píxeles excedentes de la pantalla. Si un control interactivo, un botón o un indicador de foco cae dentro de esos 32 píxeles recortados, el usuario no podrá verlo ni interactuar con él, perdiendo información visual crítica. - Origen de los 32 píxeles con
100vwycontent-box: La unidad100vwrepresenta el 100% del ancho de la ventana gráfica (viewport), incluyendo el espacio que ocupa la barra de desplazamiento vertical del navegador. Si la página tiene scroll vertical, la barra de scroll (típicamente 15 a 17 px) ya reduce el ancho disponible. Además, concontent-box, elpadding: 16pxa ambos lados agrega 16 + 16 = 32 px hacia afuera del 100vw, empujando la caja más allá del límite visible. - Solución arquitectónica: Reemplazar
width: 100vwpormax-width: 100%(que respeta el ancho del contenedor padre sin desbordar la barra de desplazamiento) y aplicarbox-sizing: border-boxuniversal para que cualquier padding se calcule hacia el interior de la caja.
Pregunta 4: Cumplimiento de WCAG 1.4.1 y arquitectura de estados de error#
- Inaccesibilidad para protanopía: Las personas con protanopía carecen de fotorreceptores para la luz roja (conos L). Un borde rojo oscuro se percibe en su campo visual como un tono marrón, gris o negro idéntico al borde inactivo normal. Si no hay otro indicador, no pueden percibir que el campo está en error.
- Tríada completa accesible:
- HTML Semántico: Un elemento textual visible
<p id="card-error" class="error-msg">que describa el error de forma explícita y contenga un icono visual diferenciador (⚠️). - Atributo ARIA de estado:
aria-invalid="true"en el<input>para que el Árbol de Accesibilidad comunique de inmediato el estado inválido al lector de pantalla al enfocar el control. - Atributo ARIA de descripción:
aria-describedby="card-error"en el<input>para conectar el texto explicativo con el campo en el AOM.
Pregunta 5: La ergonomía visual de :focus vs :focus-visible#
- Diferencia técnica:
:focusse aplica siempre que un elemento adquiere el foco, sin importar cómo se originó (clic con el ratón, pulsación táctil o navegación por teclado).:focus-visiblese aplica heurísticamente solo cuando el usuario interactúa mediante teclado o tecnologías de asistencia donde el cursor físico no está presente y el indicador de foco es estrictamente necesario para la orientación espacial. - Historia de
outline: none: Los diseñadores exigían eliminar el contorno porque al hacer clic con el ratón sobre un botón en navegadores antiguos, aparecía un anillo azul o negro que permanecía congelado alrededor del botón hasta hacer clic en otro lugar, lo cual resultaba visualmente molesto para usuarios de ratón. - La solución armónica de
:focus-visible: Permite mantener una interfaz visualmente limpia para los usuarios de ratón (que no ven ningún anillo al hacer clic) sin degradar en lo absoluto la accesibilidad, ya que en el momento en que un usuario presiona la teclaTab, el anillo:focus-visiblese activa de forma automática e impecable.
Lección 10 — TypeScript y el DOM#
1. Implementación canónica de contratos y predicados de tipo ejecutables#
El siguiente módulo TypeScript implementa el tipado formal y la validación en tiempo de ejecución para el modelo Task:
// 1. Formal Domain Types
export type TaskStatus = "open" | "done";
export type Task = {
id: number;
title: string;
status: TaskStatus;
};
// 2. Runtime Type Guard for Status Union
export function isTaskStatus(value: unknown): value is TaskStatus {
return value === "open" || value === "done";
}
// 3. Complete Runtime Type Guard for Task Entity
export function isTask(value: unknown): value is Task {
if (typeof value !== "object" || value === null) {
return false;
}
// Cast to generic record for safe property extraction
const candidate = value as Record<string, unknown>;
return (
typeof candidate.id === "number" &&
Number.isInteger(candidate.id) &&
candidate.id > 0 &&
typeof candidate.title === "string" &&
candidate.title.trim().length > 0 &&
isTaskStatus(candidate.status)
);
}
2. Parser seguro con validación sintáctica y de esquema#
export function parseTasks(jsonString: string): Task[] {
let raw: unknown;
// Syntax boundary: catch corrupted JSON strings
try {
raw = JSON.parse(jsonString);
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : "Malformed syntax";
throw new Error(`JSON parsing failure: ${detail}`);
}
// Structural boundary: verify it is an array
if (!Array.isArray(raw)) {
throw new Error("Invalid payload: Expected a top-level JSON array");
}
// Schema boundary: validate every single item in the array
for (let index = 0; index < raw.length; index++) {
const item = raw[index];
if (!isTask(item)) {
throw new Error(
`Invalid task entity at array position [${index}]: ${JSON.stringify(item)}`
);
}
}
// Control Flow Analysis: TypeScript narrows raw to Task[]
return raw;
}
3. Máquina de estados con comprobación exhaustiva (never)#
export type UIState =
| { kind: "idle" }
| { kind: "loading" }
| { kind: "success"; tasks: Task[] }
| { kind: "error"; message: string };
// Compile-time exhaustive checking utility
export function assertNever(value: never): never {
throw new Error(`Unhandled discriminated union variant: ${JSON.stringify(value)}`);
}
export function renderStatusMessage(state: UIState): string {
switch (state.kind) {
case "idle":
return "Ready to load tasks.";
case "loading":
return "Fetching tasks from server...";
case "success":
return `Loaded ${state.tasks.length} tasks successfully.`;
case "error":
return `Operation failed: ${state.message}`;
default:
// If a new state variant is added to UIState and not handled here,
// tsc will emit a compilation error:
// Argument of type '...' is not assignable to parameter of type 'never'.
return assertNever(state);
}
}
4. Manipulación segura del DOM y extracción con FormData#
export function setupTaskFormListener(
formSelector: string,
onTaskCreated: (taskData: Omit<Task, "id">) => void
): void {
const formElement = document.querySelector(formSelector);
// Runtime verification of existence and concrete subclass
if (!(formElement instanceof HTMLFormElement)) {
throw new Error(
`DOM Contract Violation: Selector "${formSelector}" did not match an HTMLFormElement.`
);
}
formElement.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
const formData = new FormData(formElement);
const rawTitle = formData.get("title");
// Narrowing FormDataEntryValue (string | File | null)
if (typeof rawTitle !== "string" || rawTitle.trim().length === 0) {
console.warn("Form validation rejected: Title field is missing or empty");
return;
}
const newTaskData: Omit<Task, "id"> = {
title: rawTitle.trim(),
status: "open",
};
onTaskCreated(newTaskData);
formElement.reset();
});
}
5. Respuestas a las preguntas de diagnóstico socrático (Lección 10)#
Pregunta 1: La trampa de la aserción ciega as Task[]#
- Ausencia de advertencias del compilador: El compilador de TypeScript no emitirá absolutamente ninguna advertencia. Al usar la palabra clave
as Task[], el programador le ordena a TypeScript que desactive sus comprobaciones y confíe en que el dato devuelto porres.json()tiene la formaTask[]. - Punto de fallo en runtime: El fallo ocurrirá mucho después, cuando la interfaz intente acceder a una propiedad renombrada o inexistente (por ejemplo, en un renderizador que haga
task.status.toUpperCase()). Comotask.statusesundefineden el objeto real de la memoria, el navegador arrojará:Uncaught TypeError: Cannot read properties of undefined (reading 'toUpperCase'), provocando la pantalla en blanco. - Por qué es una mentira al compilador: Porque
ases una instrucción sintáctica de diseño que desaparece en el archivo.jsgenerado. No ejecuta ninguna validación sobre los bytes reales recibidos desde la red.
Pregunta 2: Análisis del Análisis de Flujo de Control (CFA) en predicados de tipo#
- Por qué
booleanno estrecha el tipo: Si una función devuelve simplementeboolean, TypeScript solo sabe que la función devolviótrueofalse; no puede deducir qué relación matemática existe entre ese booleano y el tipo de los argumentos pasados en la llamada. - La magia semántica de
value is Task: La firma de predicado de tipo (value is Task) le entrega una regla formal al motor de Análisis de Flujo de Control (CFA). Le indica explícitamente: "Si esta función devuelvetrueal ejecutarse, reescribe el tipo de la variablevalueen la tabla de símbolos interna del compilador para que seaTaskdentro del bloque condicional actual".
Pregunta 3: Comprobación exhaustiva con el tipo never#
- Comportamiento durante la compilación al añadir una variante: Al añadir
{ status: "error"; message: string }, TypeScript analiza todas las ramas delswitch. Como no existe un bloquecase "error", la ejecución del análisis de flujo cae en la cláusuladefault. - Demostración matemática del paso a
never: En el análisis de tipos de unión (A ∪ B ∪ C), cada cláusulacasesustrae un subtipo del conjunto restante:
Cuando todos los casos están cubiertos, el conjunto restante es el conjunto vacío (∅), que en TypeScript es el tipo never. Sin embargo, al quedar la variante D (error) sin cubrir, la variable en el bloque default tiene tipo D. Al intentar pasar un valor de tipo D a la función assertNever(value: never), el compilador rechaza la llamada porque D no es asignable a never. El error salta en tiempo de compilación antes de enviar el código a producción.
Pregunta 4: Inyección XSS y la trampa de innerHTML#
- Ejecución del vector XSS: Al asignar la cadena con
<img src="invalid" onerror="...">ainnerHTML, el parser de HTML del navegador analiza la etiqueta, intenta cargar la imagen inexistente, falla de inmediato y ejecuta el script malicioso embebido en el atributoonerror. El atacante puede robar las cookies del usuario o su token de sesión (document.cookie) y enviarlo a su servidor sin que el usuario lo note. - Reescritura segura con
textContent:
const authorElement = document.querySelector("#author-name");
if (authorElement instanceof HTMLElement) {
// textContent trata cualquier etiqueta como texto inerte
authorElement.textContent = `Created by: ${userData.username}`;
}
Al usar textContent, el navegador no invoca el parser de HTML; la cadena <img...> se imprime en la pantalla literalmente como caracteres de texto visibles sin ejecutar código.
Pregunta 5: La anatomía de FormData.get()#
- Error del compilador: El método
formData.get(name)no devuelvestring. Según la especificación oficial del DOM, devuelve el tipo de uniónFormDataEntryValue | null, dondeFormDataEntryValue = string | File. Por lo tanto, no es asignable directamente a una variable de tipostring. - Los tres tipos posibles:
null(si el nombre del campo no existe en el formulario),File(si el control era un<input type="file">con un archivo seleccionado) ostring(para entradas de texto, radios, selects). - Código de estrechamiento seguro:
const rawTitle = formData.get("title");
if (typeof rawTitle === "string") {
const cleanTitle = rawTitle.trim();
if (cleanTitle.length > 0) {
console.log("Safe title:", cleanTitle);
}
}
Lección 11 — Estado y propiedad en React#
1. Implementación canónica del componente TaskManager en TSX#
import { useState } from "react";
export type Task = {
id: number;
title: string;
done: boolean;
};
export function TaskManager({ initialTasks }: { initialTasks: Task[] }) {
// Minimal State: only what cannot be derived and changes independently
const [tasks, setTasks] = useState<Task[]>(initialTasks);
const [query, setQuery] = useState<string>("");
// Derived State (Computed synchronously in 1 pass during render)
const normalizedQuery = query.trim().toLowerCase();
const visibleTasks = tasks.filter((task) =>
task.title.toLowerCase().includes(normalizedQuery)
);
const totalCount = tasks.length;
const completedCount = tasks.filter((t) => t.done).length;
// Immutable Toggle Handler using Functional Updater
function handleToggleTask(taskId: number) {
setTasks((previousTasks) =>
previousTasks.map((task) =>
task.id === taskId ? { ...task, done: !task.done } : task
)
);
}
return (
<section aria-labelledby="manager-title">
<h2 id="manager-title">Task Management</h2>
<p aria-live="polite">
Completed {completedCount} of {totalCount} tasks.
</p>
<label htmlFor="search-input">Search tasks</label>
<input
id="search-input"
type="search"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Filter tasks by name..."
/>
{visibleTasks.length === 0 ? (
<p>No tasks found matching your criteria.</p>
) : (
<ul>
{visibleTasks.map((task) => (
<li key={task.id}>
<label>
<input
type="checkbox"
checked={task.done}
onChange={() => handleToggleTask(task.id)}
/>
<span style={{ textDecoration: task.done ? "line-through" : "none" }}>
{task.title}
</span>
</label>
</li>
))}
</ul>
)}
</section>
);
}
2. Componente de sincronización externa con limpieza simétrica#
import { useEffect, useState } from "react";
export function EscapeKeyReset({ onReset }: { onReset: () => void }) {
useEffect(() => {
// Symmetrical handler definition
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
onReset();
}
}
window.addEventListener("keydown", handleKeyDown);
// Strict symmetrical cleanup function
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [onReset]);
return null;
}
3. Diagrama: Eliminación del doble pase de render#
flowchart TD
subgraph Antipattern["Antipatrón: useEffect + useState duplicado (2 Pases)"]
A1["Usuario escribe en input"] --> A2["setQuery('a')"]
A2 --> A3["Render 1: Pantalla muestra lista vieja sin filtrar"]
A3 --> A4["useEffect detecta cambio: setFilteredTasks(...)"]
A4 --> A5["Render 2: Pantalla finalmente filtra lista"]
end
subgraph Canonical["Canónico: Derivación Síncrona en Render (1 Pase)"]
B1["Usuario escribe en input"] --> B2["setQuery('a')"]
B2 --> B3["Render 1: const visibleTasks = tasks.filter(...) en memoria"]
B3 --> B4["Pantalla muestra lista filtrada de inmediato (Cero parpadeos)"]
end
4. Respuestas a las preguntas de diagnóstico socrático (Lección 11)#
Pregunta 1: La falacia de sincronizar estado con useEffect#
- Trazabilidad de renders: Se producen exactamente 2 renders completos.
- Render 1: React invoca la función tras
setQuery. Pinta el DOM con el nuevo valor dequery, pero con el valor anterior defiltered. - Render 2: React ejecuta el
useEffecttras pintar, el cual invocasetFiltered. Esto programa y ejecuta un segundo ciclo de renderizado completo para pintar la lista filtrada.
- Contenido dibujado en el intervalo: Durante una fracción de segundo, la pantalla muestra el campo de texto con la letra tecleada pero la lista permanece con los elementos antiguos sin filtrar, produciendo una desincronización visible (UI glitch).
- Riesgo de divergencia: Si otra función actualiza
tasksy olvida sincronizarfiltered, la interfaz mostrará una lista con tareas inexistentes o faltantes. - Refactorización canónica: Eliminar
filteredysetFiltered. Derivar la variable localmente en una sola línea dentro del cuerpo del componente:
const visibleTasks = tasks.filter(t => t.title.toLowerCase().includes(query.trim().toLowerCase()));
Pregunta 2: Inmutabilidad referencial y el algoritmo Object.is#
- Por qué no se actualiza la pantalla al mutar con
push:tasks.push(...)modifica el arreglo existente en el mismo lugar de la memoria RAM. Cuando se invocasetTasks(tasks), React compara la referencia anterior contra la nueva utilizandoObject.is(previousState, nextState). Como ambas variables apuntan exactamente a la misma dirección de memoria, React asume que el estado no ha cambiado y cancela el renderizado (bailout). - Rol de
Object.isen la reconciliación: Es la optimización primaria de React para evitar re-renderizar subárboles enteros de componentes cuando los datos permanecen invariantes. Su complejidad temporal es O(1). - Reescritura inmutable con actualizador funcional:
setTasks(prev => [...prev, { id: Date.now(), title: newTitle, done: false }]);
Pregunta 3: Instantáneas de render y temporizadores asíncronos#
- Números mostrados por las alertas: Mostrará sucesivamente:
"Count is: 0","Count is: 0","Count is: 0"(o"0","1","2"dependiendo de si los clics ocurrieron antes o después de la primera actualización programada, pero nunca mostrarán "3" en las primeras alertas). Si los 3 clics ocurren dentro del mismo render inicial antes de procesar el lote, las tres alertas capturaroncount = 0y mostrarán"Count is: 0". - Por qué muestra valores antiguos: La función pasada a
setTimeoutforma una clausura (closure) sobre las variables locales del render en el que fue creada. En esa instantánea fija de la memoria,countvalía0. El temporizador conserva la referencia a esa instantánea congelada en el tiempo. - Relación con Render as a Snapshot: Cada ciclo de render de React tiene su propia versión de constantes y funciones. React no actualiza variables existentes en llamadas previas; genera nuevas llamadas con nuevos valores.
Pregunta 4: React 18 Strict Mode y fugas de memoria silenciosas#
- Razón del doble montaje en desarrollo: React fuerza intencionalmente la secuencia
Mount -> Cleanup -> Mountpara comprobar que los efectos secundarios de los componentes sean simétricos y resilientes. Simula lo que ocurre cuando un usuario navega rápidamente de una pestaña a otra o cuando React descarta un subárbol para preservar memoria en segundo plano. - Consecuencia de olvidar el cleanup en un WebSocket: Si el componente no implementa
return () => socket.close(), el primer montaje abrirá un socket; el desmontaje no lo cerrará; y el segundo montaje abrirá un segundo socket. Quedarán 2 conexiones abiertas concurrentes consumiendo memoria y procesando eventos duplicados. - Por qué es una herramienta de diagnóstico valiosa: Porque expone en desarrollo bugs que en producción solo se manifestarían tras horas de uso continuo del usuario como lentitud inexplicable de la aplicación.
Pregunta 5: La propiedad key en listas dinámicas#
- Impacto de usar
key={index}al borrar el elemento 0: Los elementos restantes cambian de posición en el arreglo: la tarea que estaba en el índice 1 ahora pasa al índice 0, la del índice 2 al 1, etc. - Confusión del algoritmo de reconciliación: React compara el Virtual DOM anterior con el nuevo basándose en la clave. Al ver que sigue existiendo un elemento con
key={0}, React asume que la primera fila no fue eliminada, sino que simplemente cambiaron sus props. En consecuencia, conserva intacto el estado local del componente (notes) del elemento que el usuario eliminó, asociándolo erróneamente a la tarea que ahora ocupa la primera posición. - Regla de oro: La
keydebe ser un identificador único, estable y persistente provisto por el modelo de datos (como unidde base de datos o UUID). El índice del array solo es seguro si la lista es estrictamente estática (nunca se filtra, nunca se reordena y nunca se añaden o eliminan elementos).
Lección 12 — Integración de extremo a extremo#
1. Implementación canónica de fetchTask#
export type Task = {
id: number;
title: string;
status: "open" | "done";
};
function isTask(value: unknown): value is Task {
if (typeof value !== "object" || value === null) return false;
const candidate = value as Record<string, unknown>;
return (
typeof candidate.id === "number" &&
typeof candidate.title === "string" &&
(candidate.status === "open" || candidate.status === "done")
);
}
export async function fetchTask(taskId: number, signal: AbortSignal): Promise<Task | null> {
const response = await fetch(`/api/tasks/${taskId}`, { signal });
// Boundary 2A: Contractual absence (404 Not Found)
if (response.status === 404) {
return null;
}
// Boundary 2B: HTTP protocol errors (4xx / 5xx)
if (!response.ok) {
throw new Error(`Server returned error status: ${response.status}`);
}
// Boundary 2C: Empty body verification
if (response.status === 204) {
throw new Error("Unexpected empty body (204) received for entity details endpoint");
}
// Boundary 3: Deserialization and schema integrity
let payload: unknown;
try {
payload = await response.json();
} catch (err: unknown) {
throw new Error("Corrupted payload: Response body is not valid JSON");
}
if (!isTask(payload)) {
throw new Error("Schema integrity violation: Payload does not conform to Task contract");
}
return payload;
}
2. Componente React resiliente ante carreras y desconexiones#
import { useEffect, useState } from "react";
export type TaskState =
| { kind: "loading" }
| { kind: "ready"; task: Task }
| { kind: "missing" }
| { kind: "error"; message: string };
export function TaskDetailsViewer({ taskId }: { taskId: number }) {
const [state, setState] = useState<TaskState>({ kind: "loading" });
useEffect(() => {
const controller = new AbortController();
let active = true;
setState({ kind: "loading" });
fetchTask(taskId, controller.signal)
.then((task) => {
if (active) {
setState(task ? { kind: "ready", task } : { kind: "missing" });
}
})
.catch((error: unknown) => {
// Silently discard intentional cancellations
if (active && !controller.signal.aborted) {
const message = error instanceof Error ? error.message : "Network error";
console.error("TaskDetailsViewer error:", error);
setState({ kind: "error", message });
}
});
// Cleanup: cancels pending network request and invalidates closure callback
return () => {
active = false;
controller.abort();
};
}, [taskId]);
if (state.kind === "loading") {
return <p aria-busy="true">Loading task details...</p>;
}
if (state.kind === "missing") {
return <p role="status">The requested task could not be found.</p>;
}
if (state.kind === "error") {
return (
<div role="alert" style={{ color: "#b91c1c", padding: "1rem", border: "1px solid #b91c1c" }}>
<strong>Error:</strong> {state.message}
</div>
);
}
return (
<article>
<h3>{state.task.title}</h3>
<p>Status: <strong>{state.task.status}</strong></p>
</article>
);
}
3. Trazabilidad de red de CORS y configuración de Proxy en Vite#
Traza del protocolo en el cable para Preflight OPTIONS:#
Cuando el cliente (http://localhost:3000) emite un POST con application/json hacia http://localhost:8080/api/tasks:
- Solicitud Preflight enviada por el navegador:
``
http OPTIONS /api/tasks HTTP/1.1 Host: localhost:8080 Origin: http://localhost:3000 Access-Control-Request-Method: POST Access-Control-Request-Headers: Content-Type`` - Respuesta emitida por el backend (Actix Web con middleware CORS):
``
http HTTP/1.1 204 No Content Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: Content-Type Access-Control-Max-Age: 86400`` - Solicitud real enviada tras la aprobación: ```http POST /api/tasks HTTP/1.1 Host: localhost:8080 Origin: http://localhost:3000 Content-Type: application/json
{"title": "Complete documentation"}
#### Configuración de Proxy Same-Origin en Vite (`vite.config.ts`):
Para eliminar la necesidad de habilitar CORS en desarrollo, el servidor local de frontend reenvía las peticiones directamente:
// vite.config.ts import { defineConfig } from "vite"; import react from "@vitejs/plugin-react";
export default defineConfig({ plugins: [react()], server: { port: 3000, proxy: { "/api": { target: "http://localhost:8080", changeOrigin: true, }, }, }, });
---
### 4. Respuestas a las preguntas de diagnóstico socrático (Lección 12)
#### Pregunta 1: La trampa de `try/catch` con la API `fetch()`
- **Comportamiento ante un error 500:** La ejecución **no entra en el bloque `catch`**. `fetch()` solo rechaza su promesa si ocurre un fallo físico en el socket o DNS. Una respuesta `500 Internal Server Error` es una respuesta HTTP recibida con éxito; por ende, la promesa se resuelve normalmente devolviendo un objeto `Response`.
- **Valor devuelto por `loadUserData`:** Como no entra al `catch`, la función continúa, parsea el JSON del error y devuelve `{ success: true, data: { "message": "Crash" } }`.
- **Por qué oculta errores críticos:** Porque el componente visual llamador asumirá que la solicitud fue exitosa (`success: true`) e intentará renderizar los datos del perfil de usuario con el objeto de error, provocando fallos en cadena. La verificación obligatoria de `if (!response.ok)` es la única forma de capturar estos errores.
#### Pregunta 2: El desastre de la respuesta fuera de orden (*Race Condition*)
- **Secuencia cronológica:**
1. $t = 0$: Clic en Tarea 7 $\rightarrow$ Se envía petición A (tarda 600 ms).
2. $t = 200$: Clic en Tarea 8 $\rightarrow$ Se envía petición B (tarda 150 ms).
3. $t = 350$: Petición B se completa $\rightarrow$ La UI muestra los datos de la Tarea 8.
4. $t = 600$: Petición A finalmente se completa $\rightarrow$ El callback de la promesa A se ejecuta y sobreescribe la UI con los datos de la Tarea 7.
- **Por qué `AbortController` solo puede no ser suficiente:** En casos extremos, el paquete de red de la Tarea 7 ya fue procesado y su callback ya fue depositado en la cola de microtareas de JavaScript antes de que se invoque `abort()`. Aunque el socket se cierre, la microtarea se ejecutará igual.
- **Rol de la bandera `active = false`:** La bandera booleana `active` reside en la clausura léxica de ese efecto específico. Cuando React ejecuta la función de limpieza, marca `active = false`. Cuando la promesa de la Tarea 7 intente ejecutar `if (active) setState(...)`, evaluará a `false` y descartará el resultado de forma absoluta, garantizando que ninguna respuesta vieja sobreescriba el estado.
#### Pregunta 3: El crash silencioso de `response.json()` en respuestas `204 No Content`
- **Excepción lanzada:** Lanza `SyntaxError: Unexpected end of JSON input`.
- **Causa técnica:** Una respuesta `204 No Content` tiene por definición un cuerpo de longitud cero. El deserializador JSON del navegador espera al menos un token válido (`{}`, `[]`, `""`, etc.). Al encontrar un flujo vacío de bytes, el parser colapsa con error de sintaxis.
- **Responsabilidad del contrato:** El desarrollador debe inspeccionar si `response.status === 204` antes de intentar leer o deserializar el cuerpo.
#### Pregunta 4: La falacia de CORS como mecanismo de seguridad del servidor
- **Desmontando la afirmación con `curl`:** Una herramienta como `curl` o un script en Python no es un navegador web; no implementa la Política del Mismo Origen ni consulta cabeceras CORS. Un atacante ejecuta:
curl -X POST http://api.banco.com/transfer -d "amount=10000&to=attacker"
El servidor procesará la transferencia y moverá el dinero. CORS no detiene a ningún cliente automatizado.
- **Responsable de aplicar CORS:** El motor del navegador web del cliente es el único responsable de bloquear el acceso a la respuesta.
- **A quién protege la SOP:** Protege al **usuario legítimo**. Si el usuario tiene abierta su sesión de banca en una pestaña y visita un sitio web malicioso en otra pestaña, el sitio malicioso no puede usar JavaScript para leer el saldo o descargar los extractos bancarios de la cuenta del usuario gracias a la Política del Mismo Origen.
#### Pregunta 5: El peligro del comodín `Access-Control-Allow-Origin: *` con credenciales
- **Comportamiento con credenciales:** Si el cliente hace una solicitud con `credentials: "include"` (enviando cookies de sesión) y el servidor devuelve `Access-Control-Allow-Origin: *`, el navegador **bloquea la respuesta de inmediato y emite un error de seguridad en la consola**.
- **Razón de seguridad del W3C:** Permitir un comodín con credenciales permitiría a cualquier sitio web malicioso de internet hacer peticiones autenticadas contra cualquier API y leer las respuestas privadas de cualquier usuario autenticado, destruyendo por completo la privacidad en la web. La especificación exige que cuando se utilizan credenciales, el servidor debe reflejar explícitamente el origen autorizado específico en la cabecera (por ejemplo: `Access-Control-Allow-Origin: https://app.example.com`).
---
## Revisión de evidencia y matriz diagnóstica integral del frontend
| Garantía de ingeniería | Mecanismo técnico evaluado | Herramienta de auditoría / Verificación | Criterio de aceptación |
| :--- | :--- | :--- | :--- |
| **Accesibilidad de formularios** | Semántica HTML, vinculación `for`/`id`, WAI-ARIA | Teclado, DevTools Accessibility Tree, Lighthouse | Foco visible continuo, navegación con Tab, anuncio de errores sin depender solo del color. |
| **Geometría del layout** | Modelo de caja CSS (`border-box`) | DevTools Computed Styles Tab | Cero desbordamiento horizontal ($W_{\text{renderizado}} \le W_{\text{padre}}$) sin usar `overflow: hidden`. |
| **Seguridad de tipos en red** | `unknown` + Runtime Type Guard (`isTask`) | Pruebas unitarias con Jest/Vitest, `tsc` | Respuestas con campos faltantes o tipos corruptos se rechazan con error descriptivo antes de llegar a la UI. |
| **Prevención de XSS** | Uso de `textContent` sobre `innerHTML` | Auditoría de código, análisis estático ESLint | Cero inyecciones de marcado HTML externo o vectores `<img onerror>`. |
| **Estado mínimo y derivación** | Cálculo durante el render sin `useEffect` duplicado | React Profiler, DevTools Components Tab | 1 solo ciclo de renderizado al filtrar tareas; cero parpadeos visuales ni estados divergentes. |
| **Resiliencia ante carreras** | `AbortController` + bandera `active` en cleanup | Network Throttling (Fast 3G vs Slow 3G) | Seleccionar rápidamente ID 7 (lento) e ID 8 (rápido) mantiene siempre el ID 8 en pantalla. |
| **Integridad de protocolo HTTP** | Distinción de `404`, `204`, `500` y `response.ok` | Pruebas de integración HTTP / Mock Service Worker | `404` muestra estado `missing`; `204` no invoca `.json()`; `500` muestra estado `error` con reintento. |
---
## Navegación y referencias cruzadas
- Regresar a [09 · HTML, CSS y accesibilidad](../03-frontend/09-html-css-accessibility.md)
- Regresar a [10 · TypeScript y el DOM](../03-frontend/10-typescript-and-dom.md)
- Regresar a [11 · Estado y propiedad en React](../03-frontend/11-react-state.md)
- Regresar a [12 · Integración de extremo a extremo](../03-frontend/12-end-to-end-integration.md)
- Consultar el [glosario técnico](../glossary/README.md)