HextaUI

useDelayedLoading

Mostra um estado de carregamento só quando o trabalho é realmente lento e o mantém por tempo suficiente para nunca piscar.

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

Adiciona o hook e tudo de que ele depende ao seu projeto.

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

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

Passe a flag de carregamento bruta e renderize a partir do booleano que ele retorna. A maioria das requisições em uma conexão aquecida termina em menos de 150ms. Mostrar um spinner para elas é pior que não mostrar nada: ele pisca por um ou dois quadros e parece um defeito, não progresso.

O hook aplica duas regras. Espera delay antes de mostrar qualquer coisa, então o trabalho que termina antes nunca mostra um estado de carregamento. Depois de visível, o indicador permanece por pelo menos minDuration, para não aparecer e sumir em poucos quadros.

O trabalho levaDescrição
80msNada é exibido.
250msExibido aos 150ms e mantido até 550ms, o mínimo de 400ms.
900msExibido aos 150ms e ocultado assim que o trabalho termina.

O mínimo de 400ms é longo o bastante para ser percebido como um estado deliberado e curto o bastante para não atrasar ninguém.

  • Se loading voltar a ligar enquanto o indicador ainda está visível, ele simplesmente permanece visível. Não há ocultar e mostrar de novo.
  • Os temporizadores são limpos quando as entradas mudam ou o componente é desmontado, então nada atualiza o estado depois que ele sumiu.
  • No servidor e durante a primeira renderização ele retorna false, então nunca causa divergência de hidratação.

Skeletons

Skeletons substituem conteúdo, então um flash é ainda mais brusco do que com um spinner. Aqui o primeiro carregamento é lento e mostra o skeleton. Os seguintes vêm de um cache e nunca mostram.

"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,
})

Aumente delay para indicadores que cobrem boa parte da tela, como skeletons ou overlays. Diminua em direção a 0 para ações em que qualquer espera precisa ser reconhecida, como um pagamento. Mantenha minDuration acima de cerca de 300ms.

  • <Spinner loading={...} /> e <Button loading> já usam estes tempos. Recorra ao hook quando renderizar outra coisa.
  • Reserve o espaço que o indicador ocupará, como fazem os exemplos, para o layout não se deslocar quando ele aparecer.
  • Combine com um aria-busy ou uma mensagem de status. O hook apenas decide o que mostrar visualmente.
PropTipoPadrão
loadingSe o trabalho está em andamento agora.
boolean–
options.delayMilissegundos a esperar antes de mostrar o estado de carregamento.
number150
options.minDurationMínimo de milissegundos que o estado de carregamento permanece visível depois de exibido.
number400
RetornaDescrição
booleanSe deve mostrar o estado de carregamento. Sempre false no servidor.

Spinner por meio da sua prop loading.