componente · @solu30/ui-kit/data-display
DebouncedSearchInput
Buscador que retiene las teclas y avisa recién tras una pausa, para no re-renderizar la tabla entera.
Seguir leyendo — 2 párrafos más
Search input that buffers keystrokes locally and only propagates the value upward after delay ms of inactivity. This prevents heavy parent components (tables with hundreds of rows, complex filters) from re-rendering on every keystroke — only the input itself re-renders during typing.
The parent passes the *debounced* value via value; the input keeps its own local state in sync for visual feedback.
41 sitios de import en 4 de 15 verticales — un archivo que la importa cuenta una vez, monte una instancia o veinte.
- cmp-packages
- dietaly
- gestorsalon
- luigi
El componente, montado
Cargando el espécimen…
export function BuscadoresMatrix() {
const [search, setSearch] = useState('')
const [debounced, setDebounced] = useState('')
const [consultas, setConsultas] = useState(0)
const locales = RESERVAS_BUSCABLES.filter((r) => r.toLowerCase().includes(search.trim().toLowerCase()))
const remotas = RESERVAS_BUSCABLES.filter((r) => r.toLowerCase().includes(debounced.trim().toLowerCase()))
return (
<Stage label="SearchInput y DebouncedSearchInput · las dos líneas que escribe GestorSalon" tall wide>
<div className="grid w-full max-w-[620px] grid-cols-1 gap-5 px-1 py-1 sm:grid-cols-2">
<div className="flex flex-col gap-2">
<span className="text-xs text-content/50">
SearchInput · filtrado local, `debounce={'{'}0{'}'}` — la barra de la pantalla de servicios
</span>
<SearchInput
value={search}
onChange={setSearch}
placeholder="Buscar"
debounce={0}
rootClassName="w-full sm:w-60"
/>
<ul className="flex flex-col gap-1">
{locales.map((r) => (
<li key={r} className="rounded-md border border-content/10 bg-container px-2.5 py-1.5 text-[11px] text-content/70">
{r}
</li>
))}
{locales.length === 0 && <li className="px-2.5 py-1.5 text-[11px] text-content/60">Sin coincidencias.</li>}
</ul>
</div>
<div className="flex flex-col gap-2">
<span className="text-xs text-content/50">
DebouncedSearchInput · el buscador de contactos del diálogo de plantillas
</span>
<DebouncedSearchInput
value={debounced}
onDebouncedChange={(next) => {
setDebounced(next)
setConsultas((n) => n + 1)
}}
placeholder="Buscar por nombre o teléfono..."
isGlobalSearchTarget={false}
/>
<ul className="flex flex-col gap-1">
{remotas.map((r) => (
<li key={r} className="rounded-md border border-content/10 bg-container px-2.5 py-1.5 text-[11px] text-content/70">
{r}
</li>
))}
{remotas.length === 0 && <li className="px-2.5 py-1.5 text-[11px] text-content/60">Sin coincidencias.</li>}
</ul>
<span className="text-[11px] text-content/60">
Avisó {consultas} {consultas === 1 ? 'vez' : 'veces'} — una por pausa, no una por tecla.
Eso es lo que se ahorra el backend.
</span>
</div>
</div>
</Stage>
)
}De specimens/data-display.tsx · BuscadoresMatrix: el espécimen del Muestrario, no un ejemplo oficial del paquete. Muestra cómo se usa la pieza de verdad, montada contra los tipos instalados —eso es lo que vale— pero no es documentación del kit.
Cómo se usa
import { DebouncedSearchInput } from '@solu30/ui-kit/data-display'Props
| Prop | Requerida | Tipo | Qué hace |
|---|---|---|---|
| value | sí | string | External (debounced) value — used as initial + to sync on programmatic clears. |
| onDebouncedChange | sí | (next: string) => void | — |
| delay | opcional | number | Delay in ms (default 300). |
| placeholder | opcional | string | — |
| className | opcional | string | — |
| autoFocus | opcional | boolean | — |
| shortcutHint | opcional | string | F-key shortcut hint shown on the right (e.g. "F1"). |
| inputRef | opcional | React.RefObject<HTMLInputElement | null> | Optional ref forwarded to the underlying input. |
| isGlobalSearchTarget | opcional | boolean | When true (default), adds `data-search-input` and `aria-keyshortcuts="F1"` so the global useFocusSearch hook can locate this input. Set to false to opt out (e.g. secondary search inputs on the same page). |
| fullWidth | opcional | boolean | Passthrough to `TextInput`. Default `true` (unchanged). `className` lands on the `<input>`, NOT on the wrapper `TextInput` mounts — so a consumer passing `className="w-64"` shrinks the input while the wrapper stays `w-full`, leaving dead space between the input and the edge of its cell that the consumer has no way to remove. Measured in Luigi (`/rrhh/franquicias`, 2026-07-25): input at 256 px inside a 516 px wrapper. Pass `false` to let the component size to its content. NOT a fix for toolbars wrapping to extra rows — that was the initial hypothesis and the measurement refuted it: with `fullWidth={false}` the wrapper dropped to 256 px and the toolbar stayed at 3 rows, because its children add up to 1084 px in 516 px of width. That wrap is legitimate. |
Ejemplo del paquete
// Mantiene el texto tipeado al instante y avisa recién a los `delay` ms:
// `value` es el valor ya debounceado, no lo que se está escribiendo.
<DebouncedSearchInput value={q} onDebouncedChange={setQ} delay={300}
placeholder="Buscar…" shortcutHint="⌘K" />Del @example del JSDoc de @solu30/ui-kit, sin editar: son 65 de los 541 símbolos. La sangría y los renglones en blanco son los del paquete.