Ir al contenido

Hooks

La librería exporta 18 hooks. Casi todos son las piezas con las que están construidos sus propios componentes, y se publican porque hacen falta igual cuando montas algo a medida.

Todos salen de la entrada principal:

import { useToast, useDebouncedValue, useMediaQuery } from '@openfactu/ui';

Los avisos. Necesita el ToastProvider montado en la raíz. La API completa está en Feedback y estado.

const toast = useToast();
toast.success('Factura guardada');
toast.promise(guardar(), { loading: 'Guardando…', success: 'Guardado', error: 'Falló' });

Diálogos por promesa, sin montar componentes ni llevar estado. Necesita el PopupProvider. Detalle en Overlays.

const popup = usePopup();
if (await popup.confirm({ message: '¿Anular la factura?', tone: 'danger' })) {
await anular();
}

Estado y atajo global del buscador. Ver Navegación.

const paleta = useCommandPalette(); // Ctrl/⌘ + K
<SearchTrigger onOpen={paleta.openPalette} />
<CommandPalette open={paleta.open} onClose={paleta.close} sections={secciones} />

Menú del click derecho. Ver Botones y acciones.

const { contextMenu, openContextMenu } = useContextMenu();
<tr onContextMenu={(e) => openContextMenu(e, ACCIONES)}></tr>
{contextMenu}

El tema actual y cómo cambiarlo. Lanza si no hay ThemeProvider por encima; si no te importa que falte, usa useThemeOptional.

const { theme, mode, toggleMode, applyPreset, activePresetId, cssVars } = useTheme();
<Switch checked={mode === 'dark'} onChange={toggleMode} label="Modo oscuro" />
Devuelve Tipo Descripción
theme ResolvedTheme El tema completo, ya derivado
setTheme (patch | (prev) => patch) => void Cambia el tema
mode 'light' | 'dark' Modo actual
setMode (mode: ThemeMode) => void Cambia el modo
toggleMode () => void Alterna claro/oscuro
applyPreset (presetId: string) => void Aplica un preset del catálogo
activePresetId string | null Preset activo, o null si el tema es a medida
cssVars Record<string, string> Mapa --variable → valor del tema actual

El catálogo de temas: los de fábrica y los que hayan registrado los plugins. Se resuscribe al registro, así que un selector construido con esto se repinta solo en cuanto un plugin aporta o retira uno.

const presets = useThemePresets();
<div className="grid grid-cols-3 gap-2">
{presets.map((p) => (
<button key={p.id} onClick={() => applyPreset(p.id)}>{p.label}</button>
))}
</div>

Más en Temas.

El modo activo leído de la clase dark del documento, observando el atributo class de la raíz: reacciona al cambio de tema sin recargar.

const modo = useColorScheme(); // 'light' | 'dark'
const color = seriesColor(0, modo);

Acepta un elemento como argumento, para leer el modo de un subárbol tematizado aparte.

Devuelve el valor una vez ha dejado de cambiar durante delay milisegundos. Es lo que evita disparar una búsqueda en cada tecla.

const [texto, setTexto] = React.useState('');
const consulta = useDebouncedValue(texto, 300);
React.useEffect(() => { buscar(consulta); }, [consulta]);

delay vale 250 por defecto; con 0 o menos devuelve el valor sin esperar.

Carga la página siguiente cuando el final de la lista se acerca a la pantalla.

const { sentinelRef } = useInfiniteScroll({ hasMore, onLoadMore: traerMas, loading });
<tbody>
{filas.map(pintar)}
<tr ref={sentinelRef} aria-hidden />
</tbody>

Usa IntersectionObserver en lugar de la posición de scroll: medir scrollTop obliga a escuchar cada evento, falla dentro de contenedores anidados y no sabe si el final está realmente visible.

Opción Tipo Por defecto Descripción
hasMore boolean Quedan más páginas. Con false el observador se apaga
onLoadMore () => void Se llama una vez por cada entrada del centinela
loading boolean Hay una petición en vuelo: no se pide otra
disabled boolean Apaga el mecanismo (por ejemplo mientras se filtra)
rootMargin string '200px' Cuánto antes del final se dispara
root RefObject<HTMLElement> el viewport Contenedor con scroll propio

Devuelve { sentinelRef }, que vale para un <tr>, un <li> o un <div>.

Table y List ya lo llevan dentro con su prop infinite; este hook es para listas que montes tú.

Sigue una media query del navegador. Usa useSyncExternalStore, así que el primer render en servidor devuelve el valor de reserva y no hay desajuste al hidratar.

const esAncho = useMediaQuery('(min-width: 1024px)');
const apaisado = useMediaQuery('(orientation: landscape)', true);

true cuando el sistema pide reducir el movimiento.

const sinMovimiento = usePrefersReducedMotion();
<Chart type="line" animate={!sinMovimiento} />

Los cuatro con los que están construidos Modal, Drawer, DropdownMenu y compañía.

Posiciona un popover portaleado a document.body con position: fixed: coordenadas de viewport, volteo automático cuando no cabe, recorte contra los bordes, reposición al hacer scroll o redimensionar, click fuera y Escape.

const { anchorRef, popoverRef, style, placement, ready } = usePopover<HTMLButtonElement>({
open: abierto,
onClose: cerrar,
preferredPlacement: 'bottom',
matchAnchorWidth: true,
});
<button ref={anchorRef} onClick={abrir}>Abrir</button>
{abierto && createPortal(
<div ref={popoverRef} style={style} className={ready ? '' : 'invisible'}></div>,
document.body,
)}
Opción Tipo Por defecto Descripción
open boolean Abierto
onClose () => void Cerrar
preferredPlacement 'top' | 'bottom' | 'left' | 'right' 'bottom' Lado preferido; voltea si no hay espacio
align 'start' | 'end' 'start' Alineación sobre el eje transversal
offset number 4 Separación en píxeles
matchAnchorWidth boolean El popover toma el ancho del ancla (selects)
minWidth number Ancho mínimo
estimatedMaxHeight number 320 Altura estimada para decidir el volteo antes de medir
scrollStrategy 'reposition' | 'close' 'reposition' reposition para selects, close para menús
closeOnEscape boolean true Cerrar con Escape

Devuelve { anchorRef, popoverRef, style, placement, ready, update }. update() recalcula la posición a mano, por si cambia el contenido.

Mantiene un elemento montado durante la animación de salida y retrasa el estado «visible» un fotograma tras montarlo, para que la transición de entrada se dispare. Es el patrón que usan Drawer y Transition.

const { mounted, visible, duration } = useAnimatedPresence({ open: abierto });
if (!mounted) return null;
return (
<div
style={{ transitionDuration: `${duration}ms` }}
className={visible ? 'opacity-100' : 'opacity-0'}
>
</div>
);

duration vale 300 por defecto y se devuelve para que puedas sincronizar el transition-duration en línea.

Bloquea el scroll de la página mientras haya algún overlay abierto.

useScrollLock(abierto);

Lleva un contador global: con dos capas superpuestas —un diálogo abierto desde otro— cerrar la de arriba ya no desbloquea el scroll de la de abajo, que es justo lo que pasa cuando cada componente escribe body.style.overflow por su cuenta. También compensa el ancho de la barra de desplazamiento, para que el contenido no dé un salto horizontal al abrirse el overlay.

Retiene el foco dentro de un contenedor: al llegar al último elemento, Tab vuelve al primero, y Shift+Tab al revés. Al cerrar devuelve el foco a donde estaba, de modo que quien abrió el diálogo con el teclado no acaba al principio de la página.

const panelRef = React.useRef<HTMLDivElement>(null);
useFocusTrap(panelRef, {
enabled: abierto,
initialFocusRef: campoRef,
preferredRegionSelector: 'form',
});
Opción Tipo Por defecto Descripción
enabled boolean Activa la retención
initialFocusRef RefObject<HTMLElement> el primero enfocable Qué recibe el foco al abrir
restoreFocus boolean true Devuelve el foco al cerrar
preferredRegionSelector string Zona preferente para el foco inicial

Los tres restantes son variantes de otros, sin API propia que explicar.

Hook Firma Para qué
useIsNarrow (maxWidth = 640) => boolean true por debajo del ancho indicado. Atajo de useMediaQuery
useThemeOptional () => ThemeContextValue | null Como useTheme, pero devuelve null en lugar de lanzar cuando no hay proveedor
useUiTheme () => ThemeContextValue Alias de useTheme con prefijo, para aplicaciones que ya tienen su propio useTheme y necesitan importar los dos en el mismo fichero durante una migración

UiThemeProvider es el alias equivalente de ThemeProvider, por el mismo motivo.