Buscador de pacientes en un panel clínico
Un panel clínico en vanilla JavaScript muestra 3.000 pacientes. El buscador dispara una petición fetch a /api/pacientes?q= en cada tecla; el endpoint tarda entre 300 y 900 ms y no cancela peticiones anteriores. Los resultados llegan desordenados: al escribir «ana» aparece primero la respuesta de «a» y luego la de «ana». Además, al re-renderizar la lista con innerHTML se pierden los listeners de los botones «Ver ficha», y el equipo los vuelve a añadir con un bucle, lo que duplica llamadas. Hay un setInterval de 15 s que actualiza contadores y nunca se limpia al cambiar de vista. En producción se ven errores Unhandled promise rejection y algún nombre de paciente se inserta con innerHTML. El equipo no usa frameworks y debe mantener compatibilidad con Chrome y Edge actuales. La clínica pide que el buscador sea accesible por teclado y que muestre estados de carga y error.
1. Explica cómo corregir el orden incorrecto de resultados y las peticiones innecesarias, e incluye un esquema de código.
Read the worked answer
El problema es una condición de carrera: cada tecla lanza una promesa y la respuesta más lenta puede llegar después. Solución: debounce de 250–300 ms para no consultar cada tecla y AbortController para cancelar la petición anterior. Además, guardar un requestId incremental y descartar respuestas que no correspondan al último término.
let ctrl, seq = 0;
const buscar = debounce(async (q) => {
ctrl?.abort(); ctrl = new AbortController();
const id = ++seq;
setEstado('cargando');
try {
const r = await fetch(`/api/pacientes?q=${encodeURIComponent(q)}`, {signal: ctrl.signal});
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
if (id !== seq) return;
render(data);
setEstado('ok');
} catch (e) {
if (e.name !== 'AbortError') setEstado('error');
}
}, 300);
El debounce reduce carga; el abort evita respuestas viejas; el seq cubre el caso de que el abort no llegue a tiempo. También hay que manejar q vacío sin llamar a la API.
2. ¿Cómo eliminarías la pérdida y duplicación de listeners al re-renderizar, y cómo evitarías el riesgo de XSS con los nombres de pacientes?
Read the worked answer
Al usar innerHTML se destruyen nodos y sus listeners; volver a añadirlos en un bucle duplica manejadores y pierde referencias. Usar delegación: un solo listener en el contenedor #lista que lea event.target.closest('button[data-id]'). Para renderizar, crear nodos con document.createElement y textContent para nombres, o sanitizar; innerHTML con datos de pacientes permite XSS. Guardar el setInterval en una variable y llamar clearInterval en beforeunload o al cambiar de vista; mejor aún, usar visibilitychange para pausar. Si se re-renderiza, no reasignar listeners globales. La delegación también reduce memoria y funciona con filas nuevas.
3. Diseña los estados de carga, error y accesibilidad del buscador para que sea usable con teclado y lectores de pantalla.
Read the worked answer
El buscador debe ser un <input type='search'> con <label> visible o aria-label, y la lista con role='listbox'/option solo si se implementa navegación con flechas; si no, usar una lista normal y anunciar resultados. Añadir una región aria-live='polite' con «3 resultados» y un estado de carga textual; errores con role='alert'. El foco no debe perderse al actualizar; si se reemplaza la lista, mantener el foco en el input. Teclado: Enter para buscar, Escape para limpiar. Los estados de carga y error deben ser visibles y no depender solo del color.
También hay que evitar anunciar cada tecla: anunciar solo cuando termine la búsqueda. Si el usuario navega con flechas, actualizar aria-activedescendant. Probar con VoiceOver/NVDA y con navegación solo por teclado.