Planteamiento y contexto
Un cuadro de búsqueda solicita sugerencias cada vez que cambia su entrada de texto. Un usuario escribe hello rápidamente, por lo que el navegador envía solicitudes para h, he, hel, hell y hello, pero la red no garantiza que las respuestas lleguen en ese orden. La solicitud antigua puede terminar al final y reemplazar los resultados de hello con los de hell; el usuario también puede borrar la entrada, cambiar de red o abandonar la página.
Diseña el ciclo de vida de la solicitud, la regla de confirmación de resultados (commit), la estrategia de cancelación, la política de caché y frescura, los estados de carga y error, la retroalimentación accesible y el plan de verificación. Explica qué consulta representa el resultado visible en lugar de limitarte a decir "agrega debounce".
Esto encaja en entrevistas de frontend senior, React e infraestructura de interfaz de usuario. La documentación oficial de React utiliza la escritura rápida para explicar una condición de carrera (race condition) en la obtención de datos y recomienda ignorar las respuestas obsoletas durante la limpieza del Effect. MDN documenta que AbortController.abort() puede terminar un fetch, el consumo del cuerpo de una respuesta o un stream. La guía stale-while-revalidate de web.dev agrega el balance de la caché entre servir un valor antiguo aceptable mientras se actualiza. Estas fuentes respaldan la representatividad técnica del tema, pero no establecen una pregunta fija de una empresa ni una frecuencia de entrevista. La categoría es frontend porque la habilidad central reside en el estado asíncrono del lado del navegador, la consistencia del renderizado y la retroalimentación en la interacción.
Qué evalúan los entrevistadores
En primer lugar, ¿puede el candidato distinguir entre que una solicitud termine y que una solicitud aún sea apta para confirmarse (commit)? Que una Promise se complete primero no significa que represente la consulta actual. Cada solicitud necesita una identidad estable, y esa identidad o clave de consulta aún debe coincidir en el momento del commit.
En segundo lugar, ¿entienden que la cancelación es gestión de recursos? AbortController puede reducir el trabajo innecesario, pero la cancelación puede ocurrir después de que el servidor haya procesado la solicitud. El abort no es una reversión (rollback) de negocio y no puede reemplazar la protección contra resultados obsoletos.
En tercer lugar, ¿pueden modelar la máquina de estados completa? La entrada vacía, la carga inicial, la actualización con resultados existentes, el éxito, los resultados vacíos, los errores reintentables, la caché obsoleta y el desmontaje requieren reglas explícitas. Mantener los resultados antiguos mientras se carga una nueva consulta o mostrar un skeleton depende de la semántica de la consulta y del riesgo de inducir a error al usuario.
Por último, ¿pueden verificar el comportamiento controlando el orden de las respuestas y cubriendo escritura rápida, borrado de entrada, reintentos, aciertos de caché (cache hits), desmontajes y anuncios accesibles en lugar de probar únicamente respuestas exitosas que llegan en orden?
Preguntas de clarificación para hacer primero
- ¿Cada pulsación de tecla debe activar una solicitud? Si el producto espera una pausa, el debounce es útil, pero solo reduce las solicitudes y no resuelve las condiciones de carrera entre solicitudes ya enviadas.
- ¿Pueden permanecer visibles los resultados antiguos? Las sugerencias a menudo pueden permanecer durante una actualización si se etiquetan como pertenecientes a la consulta anterior; las cotizaciones financieras o los resultados de permisos podrían requerir borrarse para evitar una selección errónea.
- ¿Qué invalida la clave de caché? La cadena de consulta, los filtros, el idioma, el alcance de la cuenta y la versión de los datos pueden ser relevantes. Almacenar en caché solo por la URL de la página puede mezclar diferentes permisos o filtros.
- ¿El servidor admite cancelación o deduplicación? El cliente aún necesita protecciones de corrección; la cancelación en el servidor mejora el uso de recursos, pero no puede probar que una solicitud antigua no haya tenido efecto.
- ¿La entrada de texto necesita retroalimentación en vivo para lectores de pantalla? El conteo de resultados o los estados de carga pueden usar una región
statusexistente de forma polite, pero cada carácter individual no debería interrumpir al usuario.
Estructura de respuesta en 30 segundos
"Represento cada consulta con una clave de solicitud y solo permito que una respuesta actualice los resultados si esa clave aún coincide con la consulta actual. En la limpieza invoco AbortController.abort() para liberar trabajo de red y lectura del cuerpo, pero no trato la cancelación como una reversión; incluso si el abort falla, la verificación de identidad descarta la respuesta obsoleta. Aplicar debounce tras una pausa del usuario reduce el ruido pero no sustituye la protección contra condiciones de carrera. El estado distingue entre vacío, cargando, actualizando resultados antiguos, éxito, resultados vacíos y errores reintentables. La caché utiliza la clave de consulta completa y una ventana de frescura explícita. Las pruebas fuerzan a que una solicitud antigua retorne después de una nueva y cubren borrado, desmontaje, reintentos, almacenamiento en caché y retroalimentación para lectores de pantalla".
Respuesta a profundidad
1. Definir un invariante de confirmación de resultados (commit)
Mantén currentKey, status, visibleData y una caché opcional. currentKey incluye la consulta normalizada y cada filtro, idioma y alcance de cuenta que altere el resultado. Cada solicitud de red obtiene un requestId único; su clausura retiene su clave y su controlador.
Antes de confirmar un resultado, verifica que la solicitud no haya sido invalidada y que su clave aún sea igual a la clave actual. Solo entonces puede escribir datos visibles, un error o un estado de éxito. Este invariante es más seguro que asumir que "la última solicitud suele terminar al final", ya que la red no ofrece tal garantía de orden.
Trata la interfaz de usuario como una proyección pura de la clave de consulta actual, la entrada de caché utilizable más reciente, las solicitudes activas y los errores. Evita tener múltiples Effects sincronizando valores independientes de loading, data y error; de lo contrario, borrar o reemplazar rápidamente una consulta puede escribir un error antiguo en el nuevo estado.
2. Separar debounce, throttle y protección contra carreras
El debounce combina la entrada rápida en una sola solicitud y es útil para sugerencias. El throttle limita las solicitudes dentro de una ventana de tiempo y es útil para desplazamiento o monitoreo. Ambos controlan cuándo inicia una solicitud; ninguno evita que una solicitud ya enviada llegue tarde.
Incluso con un debounce de 250 milisegundos, el usuario puede volver a escribir mientras una solicitud está en vuelo. Mantén la verificación de identidad. El ejemplo oficial de React establece un indicador ignore en la limpieza del Effect para que una respuesta antigua ya no invoque a setResults; una secuencia incremental, una comparación de claves de consulta o una máquina de estados explícita implementan la misma regla.
3. Tratar la cancelación como optimización, no como prueba de corrección
Asigna a cada solicitud activa su propio AbortController. Cuando la consulta cambie, se borre la entrada, el componente se desmonte o comience una solicitud de reemplazo, llama a abort(). Maneja AbortError como una cancelación esperada: no lo muestres como un fallo de red ni permitas que sobrescriba el estado de la nueva consulta.
La señal puede llegar demasiado tarde para detener el procesamiento en el servidor o puede que solo detenga la lectura en el navegador. "Cancelar la solicitud antigua" y "la solicitud antigua no produjo ningún efecto en el servidor" son afirmaciones distintas. Las solicitudes GET de búsqueda normalmente no tienen efectos secundarios de escritura, pero aun así necesitan la protección de requestId; una operación irreversible necesita un contrato explícito de idempotencia.
4. Elegir entre resultados antiguos, skeletons y caché
Sin datos en la primera consulta, muestra un contenedor de carga (placeholder) y texto de estado accesible. Durante una actualización, mantén los resultados antiguos si siguen siendo seguros, etiqueta la consulta a la que pertenecen y muestra un indicador visual ligero de actualización. Si los datos antiguos pudieran provocar una selección perjudicial, bórralos o deshabilítalos para que no sean accionables.
La clave de caché debe incluir toda la entrada de la consulta. Un acierto puede renderizarse de inmediato y luego revalidarse en segundo plano, pero registra su marca temporal, origen y errores para que los datos obsoletos o con cambios de permisos no se presenten como hechos actuales. Stale-while-revalidate sirve primero un valor antiguo aceptable y obtiene uno nuevo de forma asíncrona; el producto debe definir la ventana de frescura aceptable.
5. Manejar errores, resultados vacíos y reintentos
Los resultados vacíos son un estado exitoso, no un error de red. Vincula un error a su clave actual; se descarta cualquier error proveniente de una consulta antigua. Un error reintentable conserva la consulta y la información de postergación (backoff). Un reintento crea un nuevo requestId en lugar de reutilizar una Promise ya marcada como obsoleta.
Si el servidor responde con un código 401, un alcance de permisos modificado o condiciones de consulta inválidas, limpia o revalida la caché en lugar de reintentar indefinidamente. Ante una recuperación de red, solicita únicamente la clave actual para que un evento de recuperación no repueble una consulta que el usuario ya había borrado.
6. Preservar el foco de teclado y la retroalimentación de tecnologías asistivas
No desplaces el foco del teclado cuando se actualicen los resultados. Utiliza identidades de lista estables para que desmontar resultados antiguos no obligue al lector de pantalla a releer la lista completa. Coloca los estados de carga, conteos de resultados y errores en una región status polite preexistente, y actualízala únicamente ante cambios de estado significativos.
Los usuarios de teclado deben poder seguir escribiendo, cancelar o seleccionar un resultado mientras se carga. Si un resultado es obsoleto, confirma que su clave de consulta aún coincida antes de aplicar la selección. El color no puede ser la única señal de carga o error; asocia el texto de error con la entrada o con la lista.
7. Probar la máquina de estados con orden controlado
El doble de prueba del transporte debe pausar cada solicitud y liberar las respuestas manualmente. Cubre los escenarios de una solicitud antigua teniendo éxito después de una nueva, un fallo antiguo llegando tras un éxito nuevo, una respuesta obsoleta tras borrar la entrada, una actualización en segundo plano fallida tras un acierto en caché, una respuesta después de desmontar, AbortError y volver a escribir durante un reintento.
Tras cada evento, valida mediante aserciones la clave actual, el resultado visible, el estado, la marca de tiempo de la caché y el anuncio accesible. Verifica que cargas útiles idénticas que lleguen en distinto orden no provoquen renderizados duplicados ni pérdida de foco. Monitorea el conteo de solicitudes, la tasa de cancelación, las respuestas obsoletas descartadas, la latencia del resultado visible y la tasa de reintentos, pero no reduzcas solicitudes con el fin de ocultar resultados incorrectos.
Respuesta de ejemplo de alta calidad
"Primero normalizo la consulta completa en una clave que contiene texto, filtros, idioma y alcance de permisos. Cada solicitud real obtiene un requestId y un AbortController, y registra la clave actual. Una respuesta debe superar ambas comprobaciones —que su requestId siga activo y que su clave aún coincida— antes de poder confirmar un éxito, resultados vacíos o un error. Al cambiar la entrada, cancelo el controlador antiguo y manejo silenciosamente el AbortError, pero no asumo que el abort haya revertido el procesamiento en el servidor.
Envío la solicitud tras una pausa de 250 milisegundos para reducir ruido. La carga inicial muestra un placeholder. Durante una actualización, mantengo los resultados antiguos solo cuando es seguro, etiquetándolos como pertenecientes a la consulta anterior; de lo contrario, los borro. Las entradas de caché usan la clave completa y pueden renderizarse brevemente antes de la validación en segundo plano; una entrada que exceda la ventana de frescura solo muestra el estado de carga.
La máquina de estados distingue entre entrada vacía, cargando, actualizando datos antiguos, éxito, resultados vacíos y error reintentable. Los errores se asocian a la clave, por lo que un error antiguo tardío se descarta. El foco permanece en la entrada de texto, las identidades de los resultados se mantienen estables y una región polite preexistente anuncia los cambios de estado significativos en lugar de cada carácter individual.
Luego fuerzo a que hell retorne después de hello, entrego una respuesta obsoleta tras borrar la entrada, hago fallar la cancelación, provoco el fallo de una actualización de caché, entrego una respuesta tras el desmontaje y vuelvo a escribir durante un reintento. La interfaz de usuario final debe poder explicarse únicamente mediante la clave actual".
Errores comunes
- Usar únicamente debounce → las solicitudes ya enviadas aún pueden competir en carreras → mantén una protección mediante requestId o clave.
- Mostrar la última respuesta en terminar → el orden de finalización en la red no coincide con el orden de las consultas → solo la clave actual puede confirmarse.
- Tratar el abort como una reversión → es posible que el servidor ya lo haya procesado → usa la cancelación para limpieza y reglas de identidad/versión para la corrección.
- Una sola entrada de caché para cada consulta → los filtros, idiomas o alcances pueden filtrarse entre resultados → incluye todas las entradas del resultado en la clave.
- Permitir que un error antiguo sobrescriba una nueva consulta → los usuarios ven un fallo obsoleto → vincula los errores al requestId y a la clave.
- Dejar la interfaz en blanco en cada carga → los usuarios pierden contexto útil → elige entre resultados antiguos o un skeleton según el riesgo de datos engañosos.
- Anunciar cada pulsación de tecla → los lectores de pantalla sufren interrupciones continuas → anuncia únicamente cambios significativos de carga, resultados y errores.
- Probar solo respuestas exitosas y ordenadas → las condiciones de carrera reales quedan omitidas → controla el orden de respuesta, cancelación, borrado, desmontaje y reintento.
Preguntas de seguimiento y respuestas
¿Basta una clave para la paginación o el desplazamiento infinito?
Incluye filtros, ordenamiento, cursor o página en la clave de solicitud y vincula cada página a una versión de consulta. Una nueva consulta descarta las páginas anteriores. Cargar otra página para la misma consulta puede mantener visible la página previa, pero los cursores duplicados y las páginas obsoletas no pueden sobrescribir la lista confirmada. Utiliza un cursor siguiente provisto por el servidor en lugar de deducir el orden a partir de un número de página en el cliente.
¿Qué cambia cuando el usuario escribe sin conexión y se reconecta?
Por lo general, la búsqueda no necesita persistir todas las solicitudes antiguas; conserva la entrada actual y la última caché aceptable. Al reconectarse, solicita únicamente la clave actual e ignora el trabajo offline obsoleto. Si las sugerencias sin conexión son un requisito del producto, la antigüedad de la caché, el alcance de los datos y la frescura deben ser visibles para que los resultados sin conexión no se presenten como información en tiempo real.
¿Pueden React Query o SWR resolver esto por sí solos?
Una biblioteca de caché puede proporcionar deduplicación, almacenamiento en caché, invalidación y gestión del ciclo de vida, pero el producto sigue definiendo la clave de consulta, la obsolescencia aceptable, los reintentos y los estados de interacción. La cancelación de la biblioteca o un tiempo de obsolescencia predeterminado no pueden determinar el alcance de permisos, el riesgo de datos engañosos ni la retroalimentación accesible. Verifica la semántica de condiciones de carrera de la biblioteca y codifica las reglas en la clave de consulta y en el estado de la UI.
¿Es segura la cancelación si la búsqueda pasa a ser un POST con registro de auditoría?
No des por sentado que lo sea. Un POST puede tener efectos secundarios en el servidor y una cancelación del cliente no revierte una transacción. Utiliza una clave de idempotencia, un estado de envío explícito y un endpoint para consultar el estado; muestra el éxito solo tras una confirmación autoritativa. Un POST sigue siendo razonable para una consulta compleja sin efectos secundarios, pero su contrato de reintento y cancelación debe ser explícito.