HextaUI

useDelayedLoading

Muestra un estado de carga solo cuando el trabajo es realmente lento, y lo mantiene el tiempo suficiente para que nunca parpadee.

loading
useDelayedLoading(loading)
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Spinner } from "@/components/ui/spinner"
import { useDelayedLoading } from "@/hooks/use-delayed-loading"

function Lane({ label, loading }: { label: string; loading: boolean }) {
  return (
    <div className="flex h-9 items-center justify-between gap-4 rounded-lg bg-muted px-3 text-sm">
      <span className="text-muted-foreground">{label}</span>
      <span className="flex size-4 items-center justify-center">
        {loading ? <Spinner /> : null}
      </span>
    </div>
  )
}

export function UseDelayedLoadingDemo() {
  const [loading, setLoading] = React.useState(false)
  const visible = useDelayedLoading(loading)
  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)

  React.useEffect(() => () => clearTimeout(timer.current), [])

  const load = (ms: number) => {
    clearTimeout(timer.current)
    setLoading(true)
    timer.current = setTimeout(() => setLoading(false), ms)
  }

  return (
    <div className="flex w-full max-w-xs flex-col gap-4">
      <div className="flex flex-col gap-2">
        <Lane label="loading" loading={loading} />
        <Lane label="useDelayedLoading(loading)" loading={visible} />
      </div>
      <div className="flex flex-wrap justify-center gap-2">
        <Button variant="outline" size="sm" onClick={() => load(80)}>
          80ms
        </Button>
        <Button variant="outline" size="sm" onClick={() => load(220)}>
          220ms
        </Button>
        <Button variant="outline" size="sm" onClick={() => load(1500)}>
          1.5s
        </Button>
      </div>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/use-delayed-loading.json

Añade el hook y todo lo que necesita a tu proyecto.

import { useDelayedLoading } from "@/hooks/use-delayed-loading"
const { data, isFetching } = useQuery(query)
const showSpinner = useDelayedLoading(isFetching)

return showSpinner ? <Spinner /> : <Results data={data} />

Pasa el indicador de carga sin procesar y renderiza a partir del booleano que devuelve. La mayoría de las peticiones en una conexión caliente terminan en menos de 150ms. Mostrar un spinner para ellas es peor que no mostrar nada: parpadea durante un fotograma o dos y se lee como un fallo, no como progreso.

El hook aplica dos reglas. Espera delay antes de mostrar nada, así que el trabajo que termina antes nunca muestra un estado de carga. Una vez visible el indicador, permanece al menos minDuration, para que no pueda aparecer y desaparecer en pocos fotogramas.

El trabajo tardaDescripción
80msNo se muestra nada.
250msSe muestra a los 150ms y se mantiene hasta los 550ms, el mínimo de 400ms.
900msSe muestra a los 150ms y se oculta en cuanto termina el trabajo.

El mínimo de 400ms es lo bastante largo para percibirse como un estado deliberado y lo bastante corto para no ralentizar a nadie.

  • Si loading vuelve a activarse mientras el indicador sigue visible, simplemente permanece visible. No se oculta y se vuelve a mostrar.
  • Los temporizadores se limpian cuando cambian las entradas o el componente se desmonta, así que nada actualiza el estado una vez desaparecido.
  • En el servidor y durante el primer renderizado devuelve false, así que nunca añade una discrepancia de hidratación.

Skeletons

Los skeletons sustituyen contenido, así que un parpadeo es aún más molesto que con un spinner. Aquí la primera carga es lenta y muestra el skeleton. Las cargas posteriores vienen de una caché y nunca lo muestran.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
import { useDelayedLoading } from "@/hooks/use-delayed-loading"

const people = ["Ada Lovelace", "Grace Hopper", "Alan Turing"]

export function UseDelayedLoadingSkeleton() {
  const [loading, setLoading] = React.useState(false)
  const [cached, setCached] = React.useState(false)
  const showSkeleton = useDelayedLoading(loading, {
    delay: 200,
    minDuration: 500,
  })

  const refresh = () => {
    setLoading(true)
    setTimeout(
      () => {
        setLoading(false)
        setCached(true)
      },
      cached ? 60 : 1200
    )
  }

  return (
    <div className="flex w-full max-w-xs flex-col gap-4">
      <ul className="flex flex-col gap-3">
        {people.map((name) => (
          <li key={name} className="flex h-5 items-center text-sm">
            {showSkeleton ? <Skeleton className="h-3 w-32" /> : name}
          </li>
        ))}
      </ul>
      <Button variant="outline" size="sm" onClick={refresh} disabled={loading}>
        {cached ? "Refresh (cached)" : "Refresh (slow)"}
      </Button>
    </div>
  )
}
const showSkeleton = useDelayedLoading(isLoading, {
  delay: 300,
  minDuration: 600,
})

Sube delay para indicadores que cubren gran parte de la pantalla, como skeletons u overlays. Bájalo hacia 0 para acciones en las que cualquier espera necesita reconocerse, como un pago. Mantén minDuration por encima de unos 300ms.

  • <Spinner loading={...} /> y <Button loading> ya usan estos tiempos. Recurre al hook cuando renderices otra cosa.
  • Reserva el espacio que ocupará el indicador, como hacen los ejemplos, para que el diseño no se desplace cuando aparezca.
  • Combínalo con un aria-busy o un mensaje de estado. El hook solo decide qué mostrar visualmente.
PropTipoPredeterminado
loadingSi el trabajo está en curso ahora mismo.
boolean–
options.delayMilisegundos de espera antes de mostrar el estado de carga.
number150
options.minDurationMilisegundos mínimos que el estado de carga permanece visible una vez mostrado.
number400
DevuelveDescripción
booleanSi se debe mostrar el estado de carga. Siempre false en el servidor.

Spinner mediante su prop loading.