Contexto y Alcance
Dado un API de búsqueda, diseña un componente de autocompletado reutilizable. Debe solicitar como máximo 10 sugerencias después de que el usuario deje de escribir durante 300 ms, y el API tiene una latencia p95 de 400 ms. En escritorio y móvil, debe ser compatible con teclado, ratón, toque, lectores de pantalla y entrada IME, y nunca debe mostrar resultados de una consulta anterior durante la escritura rápida. Explica el API del componente, el modelo de estado, las solicitudes asíncronas, la semántica de accesibilidad, el caché, el manejo de errores y las pruebas.
Esos números son supuestos de la entrevista. El alcance base es el componente frontend; el ranking del lado del servidor, la corrección ortográfica, la personalización, la selección múltiple y el desplazamiento infinito están fuera del alcance. Un resultado puede ser texto plano o una fila con renderizado personalizado, pero cada resultado debe tener un ID estable y una etiqueta legible. El componente sigue un patrón de combobox editable con una lista de sugerencias de selección única.
Esta pregunta aplica para roles de frontend y full-stack. Sus habilidades centrales son la interfaz de usuario del navegador, el estado asíncrono y la interacción accesible, por lo que la categoría es frontend. El artículo existente sobre Trie trata la búsqueda por prefijo como un problema de estructura de datos y menciona Top K solo como seguimiento. Aquí el API de búsqueda es una dependencia dada; el problema independiente son las condiciones de carrera del lado del cliente, la semántica del foco, el comportamiento IME y un contrato de interacción verificable.
Lo que Evalúa el Entrevistador
La primera señal es si el candidato separa la reducción de solicitudes de la corrección. Un debounce de 300 ms reduce las solicitudes durante la escritura continua, pero no impide que una solicitud anterior regrese después de una más reciente. Una respuesta sólida combina la cancelación con una secuencia de solicitudes monotónicamente creciente y permite que solo la solicitud más reciente para la consulta actual confirme resultados.
La segunda señal es si la semántica de accesibilidad forma un contrato completo. El estilo visual por sí solo no puede relacionar el input, el popup y las opciones. El foco del DOM debe permanecer en el input mientras aria-activedescendant identifica la opción activa. aria-expanded, aria-controls, aria-autocomplete, listbox, option y aria-selected deben cambiar de manera consistente con el estado visible.
La tercera señal es un modelo preciso de la entrada de texto. Los IME de chino, japonés y otros idiomas producen varias actualizaciones durante una sesión de composición. Buscar cada cadena intermedia crea solicitudes irrelevantes y puede interferir con la selección de candidatos. El componente debe rastrear la composición y programar una búsqueda para el valor confirmado después de compositionend.
La cuarta señal es si el estado y la interfaz de usuario pueden demostrarse consistentes. Un único booleano loading y un arreglo no pueden expresar claramente una consulta corta, cargando, éxito, resultados vacíos, fallo y un popup cerrado. Una respuesta sólida define transiciones, IDs de opciones estables, comportamiento de recuperación y la regla para la opción activa después de que cambian los resultados, y luego prueba esos invariantes con respuestas fuera de orden, un teclado y un lector de pantalla.
Preguntas de Clarificación Antes de Responder
- ¿Qué ocurre después de elegir una sugerencia? Si solo rellena el input, llama a
onSelecty cierra la lista. Si navega inmediatamente, el fallo de navegación y la restauración del input al regresar necesitan su propio contrato. El diseño base rellena el input y emite un callback. - ¿Quién controla el valor del input? Un formulario puede requerir
valueyonValueChange; un cuadro de búsqueda independiente puede admitir un valor inicial no controlado. El componente no debe cambiar de modo durante su ciclo de vida. - ¿Los resultados son texto plano? Los resultados enriquecidos requieren
renderItem, perogetKeyygetLabelaún deben proporcionar un ID estable y un nombre accesible. Aceptar HTML arbitrario aumenta el riesgo de inyección. - ¿Cuántos caracteres inician una búsqueda? El valor predeterminado base es dos. Por debajo de ese umbral, cancela el trabajo pendiente, cierra la lista y limpia la opción activa. Mostrar historial para una consulta vacía requeriría un
aria-autocompletey una política de caché diferentes. - ¿Tab selecciona la sugerencia activa? En el diseño base, no; Tab sale del widget. Si el producto insiste en que Tab acepte una sugerencia, ese comportamiento necesita comunicación explícita al usuario y pruebas separadas, en lugar de anular silenciosamente el movimiento de foco normal.
- ¿Deben permanecer los resultados anteriores después de un error? Este diseño los limpia y muestra un error reintentable para que los usuarios no confundan las sugerencias de una consulta anterior con la consulta actual. Un producto con capacidad offline podría mostrar una entrada de caché marcada como posiblemente desactualizada, pero ese es un contrato diferente.
Marco de Respuesta de 30 Segundos
"Separaría el renderizado personalizable de un controlador de estado sin interfaz. El controlador posee la consulta, el estado de la solicitud, el estado del popup, el ID de la opción activa, el estado IME y la secuencia de solicitud más reciente. Después de la entrada confirmada, aplica un debounce de 300 milisegundos y cancela la solicitud anterior; una respuesta aún debe coincidir tanto con la secuencia más reciente como con la consulta actual antes de poder confirmar. El input usa semántica de combobox y mantiene el foco del DOM, mientras que las teclas de flecha actualizan aria-activedescendant, Enter selecciona y Escape cierra. La búsqueda espera a que termine la composición, y un mensaje de estado separado anuncia cargando, cantidad de resultados y sin resultados. Verificaría esto con respuestas de red reordenadas, una matriz de teclado, entrada IME, un lector de pantalla e invalidación de caché."
Análisis Paso a Paso
Paso 1: Definir el límite y el API público
Los algoritmos de búsqueda pertenecen al servidor. El componente recibe una función de consulta y una política de renderizado. Una interfaz neutral al framework podría verse así:
Autocomplete<T>({
value,
onValueChange,
fetchSuggestions(query, signal),
getKey(item),
getLabel(item),
renderItem,
onSelect,
minChars = 2,
limit = 10,
debounceMs = 300,
})fetchSuggestions acepta un AbortSignal para que el llamador pueda propagar una cadena de cancelación a la red. getKey proporciona un ID de DOM de opción estable, y getLabel proporciona tanto el texto de relleno como un nombre accesible. El renderizado personalizado no debe reemplazar la semántica de teclado, foco o selección. Si el componente expone callbacks de cambio de valor, incluye una razón estructurada como input, selection o clear para que los consumidores no infieran la intención del usuario a partir de cambios en las cadenas.
Paso 2: Restringir el renderizado con una máquina de estados
El estado central es:
query status = idle | loading | success | empty | error items isOpen activeId selectedItem isComposing latestRequestSeq
query es el texto actual del input; selectedItem es la selección confirmada. Son hechos distintos y no deben compartir un mismo campo. La lista puede abrirse solo cuando la consulta cumple la longitud mínima, el input aún está en un contexto interactivo y el estado tiene resultados o retroalimentación para mostrar. Limpia activeId cuando comienza una nueva consulta. Cuando llegan nuevos resultados, conserva un ID activo anterior solo si todavía existe; de lo contrario, límpialo para que aria-activedescendant nunca haga referencia a un nodo faltante.
empty y error son estados distintos. Una respuesta vacía es válida; un error puede reintentarse. Cerrar el popup no tiene que destruir la consulta o el caché, pero debe restablecer isOpen y activeId.
Paso 3: Separar debounce, cancelación y confirmación de resultados
Un evento de input primero actualiza query de manera síncrona. Si la composición está activa, la consulta normalizada tiene menos de dos caracteres, o es solo espacios en blanco, limpia el temporizador, aborta la solicitud actual y restablece la lista. De lo contrario, inicia la solicitud después de 300 ms. El flujo de solicitud puede expresarse como pseudocódigo:
async function search(rawQuery) { const query = normalize(rawQuery) const seq = ++latestRequestSeq
controller?.abort() controller = new AbortController() setStatus("loading")
try { const items = await fetchSuggestions(query, controller.signal) if (seq !== latestRequestSeq || query !== normalize(currentQuery)) return commit(items.slice(0, 10)) } catch (error) { if (isAbort(error)) return if (seq === latestRequestSeq && query === normalize(currentQuery)) { commitError(error) } } }
AbortController puede detener una solicitud Fetch en curso y el consumo del cuerpo de respuesta, lo que ahorra trabajo. La secuencia de solicitudes es la puerta de corrección. Una operación anterior puede ya haberse completado, o un llamador puede usar una capa de datos que no respeta completamente la señal, por lo que llamar a abort() solo no prueba que los resultados obsoletos no puedan sobrescribir los frescos.
Con un debounce de 300 ms y un p95 del API de 400 ms, el camino p95 desde la última pulsación de tecla hasta los resultados es de aproximadamente 700 ms antes del renderizado. Esta derivación hace explícito el compromiso. Si la experiencia necesita ser más rápida, usa un caché de consulta exacta o reduce el debounce después de medir el costo de las solicitudes; no afirmes que un debounce de 300 ms puede producir una respuesta sin caché de 150 ms.
Paso 4: Manejar IME, puntero y foco correctamente
compositionstart establece isComposing en verdadero. Durante la composición, input actualiza el texto visible pero no programa una búsqueda. compositionend limpia el indicador y programa una búsqueda para el texto final confirmado. Los eventos de teclado pueden ocurrir mientras la composición está activa e informar isComposing, por lo que Enter no debe seleccionar una sugerencia en ese momento. Prueba el envoltorio de eventos del framework en los navegadores de destino en lugar de asumir que la entrada en inglés de escritorio representa cada secuencia.
La selección con ratón y toque debe considerar que el input puede perder el foco antes de que se ejecute un clic, lo que podría eliminar la opción clickeada. El pointerdown principal puede retener el foco del input, mientras que un click o un pointerup que se mantuvo dentro de un umbral de movimiento completa la selección a través de un camino compartido. Desplazarse por la lista no debe convertir el movimiento ordinario del puntero en una selección. Un clic fuera cierra el popup, mientras que el callback de selección todavía se ejecuta exactamente una vez.
El foco del DOM permanece en el input. Las opciones del popup están excluidas de la secuencia de Tab, y Tab sigue el comportamiento predeterminado del navegador para salir del widget. Esto mantiene estable el comportamiento nativo de edición de texto y los modos de entrada de tecnología de asistencia.
Paso 5: Implementar el combobox y el contrato de teclado
El input tiene un label visible, u obtiene su nombre a través de aria-labelledby o aria-label. Usa role="combobox", aria-autocomplete="list", aria-expanded sincronizado con el popup, aria-controls apuntando a la lista, y aria-activedescendant solo mientras exista una opción activa.
El contenedor de sugerencias usa role="listbox". Cada ítem tiene role="option" y un ID de DOM estable, y el ítem visualmente activo también tiene aria-selected="true". La flecha hacia abajo hace activa la primera opción y luego se mueve hacia abajo; la flecha hacia arriba se mueve en sentido inverso mientras el foco del DOM permanece en el input. El diseño base se detiene en los límites en lugar de hacer un ciclo. Enter acepta la opción activa. Escape cierra el popup pero preserva el texto del input.
No interceptes incondicionalmente Left, Right, Home, End, Backspace ni caracteres imprimibles. Un combobox editable debe preservar el comportamiento de edición nativa de una sola línea del navegador. Llama a preventDefault() solo cuando el componente realmente maneja el movimiento de la lista, la selección o el descarte.
La lista de resultados no necesita convertirse en una región live de alta prioridad en cada actualización. Usa un elemento role="status" separado para anunciar "Buscando", "10 resultados" o "Sin resultados" sin mover el foco. Anunciar en cada pulsación de tecla puede hacer que el widget sea excesivamente verboso.
Paso 6: Acotar el caché y la recuperación de fallos
Una clave de caché incluye al menos la consulta normalizada, la configuración regional, los filtros y la versión de los datos. El caché solo por consulta devuelve datos incorrectos después de un cambio de configuración regional o de filtros. Usa un caché de consulta exacta con un TTL y un límite de capacidad. Un acierto puede renderizarse inmediatamente, seguido de una actualización en segundo plano solo si la política de frescura del producto lo requiere.
Las respuestas en caché aún pasan las verificaciones de consulta actual y secuencia de solicitudes. Limpiar el input, seleccionar un resultado, desmontar el componente o cambiar una dependencia cancela los temporizadores y las solicitudes. Un error del servidor expone una pequeña acción de reintento; no se convierte en una fila seleccionable en la lista de sugerencias. El reintento crea una nueva secuencia de solicitudes, por lo que un error anterior no puede sobrescribir un éxito posterior.
Resalta las coincidencias con nodos de texto o fragmentos estructurados de confianza, no con marcado del servidor sin sanitizar. Codifica correctamente los parámetros de consulta y no registres consultas sensibles completas a menos que sean genuinamente necesarias.
Paso 7: Verificar invariantes con escenarios adversariales
Las pruebas unitarias con un reloj controlado demuestran que 299 ms no causa ninguna solicitud, 300 ms causa una solicitud, continuar escribiendo cancela el temporizador anterior, y una consulta de menos de dos caracteres restablece el estado. Las pruebas de estado cubren éxito, resultados vacíos, error, reintento, cierre y selección.
Una prueba de condición de carrera hace que la respuesta lenta para a llegue después de la respuesta rápida para ab; la lista final puede contener solo los resultados de ab. Prueba tres caminos por separado: una solicitud cancelable, una solicitud ya completada, y una capa de datos que ignora la señal de cancelación.
Las pruebas de interacción cubren flechas, Enter, Escape, Tab, clic, toque, clic fuera, composición y una opción activa que desaparece después de que cambian los resultados. Cada paso verifica el estado visible, el valor del input, el conteo de callbacks, el foco del DOM y los atributos ARIA en conjunto.
Los análisis automáticos de accesibilidad capturan solo parte del contrato. Usa un teclado y un lector de pantalla de destino para completar la entrada, la carga, el anuncio de resultados, la selección, los resultados vacíos y la recuperación de errores. Prueba el toque y el teclado de software en los navegadores móviles compatibles. Las pruebas de rendimiento registran la distribución desde la última pulsación de tecla hasta el primer resultado interactivo, la tasa de cancelación de solicitudes, la tasa de aciertos de caché y el conteo de respuestas obsoletas descartadas.
Respuesta de Muestra de Alta Calidad
"Limitaría esto a un componente frontend de sugerencias de selección única; un API existente posee la búsqueda y el ranking. El componente acepta un valor controlado, función de consulta, clave estable, etiqueta legible, renderizador de fila personalizado y callback de selección. Internamente, el texto de la consulta, el objeto elegido, el estado de la solicitud, el estado del popup, la opción activa, el estado IME y la secuencia de solicitudes permanecen separados, de modo que los resultados vacíos, los errores y un popup cerrado no colapsan en un solo booleano.
El input se actualiza inmediatamente. Una vez que tiene dos caracteres y la composición ha terminado, aplico un debounce de 300 milisegundos, cancelo la solicitud anterior e inicio una solicitud secuenciada. Una respuesta puede confirmarse solo si su secuencia sigue siendo la más reciente y su consulta aún coincide con el input. La cancelación ahorra trabajo; el secuenciamiento garantiza la corrección. Los supuestos de 300 más 400 milisegundos sitúan el camino p95 sin caché en aproximadamente 700 milisegundos, por lo que la latencia real y el costo de las solicitudes deben determinar el debounce.
Semánticamente, el input con nombre es un combobox que controla un listbox. El foco del DOM permanece en el input; las teclas de flecha cambian un aria-activedescendant estable, Enter selecciona, Escape cierra y Tab sale normalmente. Las opciones usan option y aria-selected, mientras que una región de estado separada anuncia cargando, cantidad de resultados y sin resultados. La búsqueda y la selección con Enter se suspenden durante la composición.
El caché está acotado por TTL y capacidad, y tiene como clave la consulta normalizada, la configuración regional y los filtros. Verificaría las respuestas lentas-antiguas versus rápidas-nuevas, IME, el comportamiento de teclado y lector de pantalla, el desenfoque por puntero, los resultados vacíos, el reintento y la invalidación de caché. El éxito significa que cada resultado visible pertenece a la consulta actual y el estado de foco y accesibilidad coincide con el estado visual en cada transición."
Errores Comunes
- Solo usar debounce → El debounce reduce las solicitudes pero no ordena las respuestas → Agrega cancelación y condiciona la confirmación tanto en la secuencia más reciente como en la consulta actual.
- Solo llamar a
abort()→ El trabajo anterior puede ya estar completo o la capa de datos puede ignorar la señal → Trata la cancelación como una optimización y la verificación de secuencia como la condición de corrección. - Usar un índice de arreglo como ítem activo → El mismo índice puede identificar un resultado diferente después de reordenar → Usa un ID de resultado estable y confirma que aún existe después de reemplazar la lista.
- Mover el foco del DOM a cada opción → El foco del input, la edición de texto y la tecnología de asistencia pueden divergir → Mantén el foco en el input y expón el ítem activo a través de
aria-activedescendant. - Interceptar cada evento de teclado → El comportamiento nativo de Home, End, flecha y edición IME se rompe → Intercepta solo las teclas que el componente realmente usa para la navegación de la lista, la selección o el descarte.
- Buscar en cada actualización IME → El texto fonético no confirmado produce solicitudes incorrectas y Enter puede seleccionar accidentalmente → Rastrea la sesión de composición y busca solo después de que termine.
- Hacer que toda la lista de resultados sea un anuncio urgente → Cada actualización del input genera habla verbosa → Usa un mensaje
role="status"moderado para cargando, conteo y sin resultados. - Cachear solo por consulta → Los cambios de configuración regional o de filtros pueden acceder a datos incorrectos → Incluye cada condición que afecte los resultados y la versión en la clave, con límites de TTL y capacidad.
Seguimientos y Cómo Manejarlos
Seguimiento 1: ¿Qué cambia si la lista debe mostrar 1,000 resultados?
Cuestiona primero el requisito: el autocompletado normalmente debería clasificar y limitar un pequeño conjunto de sugerencias relevantes. Si se requiere explorar un conjunto grande, agrega windowing. La opción activa debe permanecer montada, o desplazarse dentro de la ventana montada antes de actualizar aria-activedescendant; el atributo no puede hacer referencia a un nodo que fue virtualizado y eliminado. Expón semántica significativa de posición y tamaño total y verifícala con un lector de pantalla, no solo con una medición de velocidad de fotogramas.
Seguimiento 2: ¿Cómo pueden múltiples páginas compartir solicitudes y entradas de caché?
Mueve el almacenamiento de caché y la deduplicación de solicitudes a una capa de datos a nivel de página mientras el componente aún consume solo fetchSuggestions. Las claves compartidas incluyen la configuración regional, los filtros, el alcance de autorización y la versión de los datos. Las llamadas para la misma clave pueden compartir una Promise, pero cada componente mantiene su propia secuencia de solicitudes porque dos inputs pueden tener diferentes consultas actuales y tiempos de desmontaje.
Seguimiento 3: ¿Cómo agregarías historial local y sugerencias para consultas vacías?
Marca cada fuente como history o remote, luego define la eliminación, la privacidad y la sincronización entre dispositivos. El historial en una consulta vacía es un estado diferente al de completar la entrada escrita, por lo que necesita un aria-autocomplete explícito, un encabezado y una política de anuncios. La selección del historial aún sigue el contrato de ID estable, y las búsquedas sensibles no deben persistirse de manera predeterminada.
Seguimiento 4: ¿Cómo funcionaría esto con renderizado en servidor e hidratación?
El servidor puede renderizar la etiqueta y el input vacío, mientras que el cliente posee el popup interactivo. Los IDs del input, la lista y las opciones deben ser estables entre el servidor y el cliente; los IDs aleatorios en el primer renderizado pueden romper aria-controls después de la hidratación. Si el servidor precarga sugerencias, serializa la clave de caché, la consulta y la versión de datos juntas y reutiliza la carga útil solo cuando los tres aún coinciden.
Seguimiento 5: ¿Cómo cambiarías el componente para admitir chips de selección múltiple?
Eso cambia el foco, la eliminación y la semántica de ítems seleccionados; reemplazar selectedItem con un arreglo es insuficiente. Define el movimiento izquierdo y derecho entre el input y los chips, la confirmación con Backspace, los duplicados, un conteo máximo y los anuncios del lector de pantalla. La lista de sugerencias puede seguir siendo un combobox, pero los chips seleccionados necesitan su propia región navegable y una nueva ronda de pruebas de teclado y tecnología de asistencia.