HextaUI

useDelayedLoading

Zeigt einen Ladezustand nur, wenn die Arbeit tatsächlich langsam ist, und hält ihn dann lange genug, damit er nie flackert.

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

Fügt den Hook und alles, wovon er abhängt, zu deinem Projekt hinzu.

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

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

Übergib das rohe Loading-Flag und rendere aus dem zurückgegebenen Boolean. Die meisten Anfragen über eine warme Verbindung enden in unter 150ms. Dafür einen Spinner zu zeigen, ist schlechter als nichts zu zeigen: Er blitzt für einen oder zwei Frames auf und wirkt wie ein Glitch, nicht wie Fortschritt.

Der Hook wendet zwei Regeln an. Er wartet delay, bevor er etwas anzeigt, sodass Arbeit, die früher endet, nie einen Ladezustand zeigt. Sobald der Indikator sichtbar ist, bleibt er mindestens minDuration, sodass er nicht innerhalb weniger Frames erscheinen und verschwinden kann.

Arbeit dauertBeschreibung
80msEs wird nichts angezeigt.
250msBei 150ms angezeigt und bis 550ms gehalten, dem 400ms-Minimum.
900msBei 150ms angezeigt und sofort ausgeblendet, wenn die Arbeit endet.

Das 400ms-Minimum ist lang genug, um als bewusster Zustand wahrgenommen zu werden, und kurz genug, um niemanden zu bremsen.

  • Schaltet loading wieder ein, solange der Indikator noch sichtbar ist, bleibt er einfach sichtbar. Es gibt kein erneutes Ausblenden und Einblenden.
  • Timer werden gelöscht, wenn sich die Eingaben ändern oder die Komponente unmountet, sodass nichts State aktualisiert, nachdem sie weg ist.
  • Auf dem Server und beim ersten Render gibt er false zurück und führt daher nie zu einem Hydration-Mismatch.

Skeletons

Skeletons ersetzen Inhalt, daher ist ein Aufblitzen noch störender als bei einem Spinner. Hier ist der erste Ladevorgang langsam und zeigt das Skeleton. Spätere Ladevorgänge kommen aus einem Cache und zeigen es nie.

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

Erhöhe delay bei Indikatoren, die viel vom Bildschirm bedecken, wie Skeletons oder Overlays. Senke es Richtung 0 bei Aktionen, bei denen jede Wartezeit bestätigt werden muss, etwa einer Zahlung. Halte minDuration über etwa 300ms.

  • <Spinner loading={...} /> und <Button loading> nutzen diese Timings bereits. Greife zum Hook, wenn du etwas anderes renderst.
  • Halte den Platz frei, den der Indikator einnehmen wird, wie es die Beispiele tun, damit sich das Layout beim Erscheinen nicht verschiebt.
  • Kombiniere ihn mit einem aria-busy oder einer Statusmeldung. Der Hook entscheidet nur, was visuell angezeigt wird.
PropTypStandard
loadingOb die Arbeit gerade läuft.
boolean–
options.delayMillisekunden, die vor dem Anzeigen des Ladezustands gewartet wird.
number150
options.minDurationMinimale Millisekunden, die der Ladezustand sichtbar bleibt, sobald er angezeigt wird.
number400
RückgabeBeschreibung
booleanOb der Ladezustand gezeigt werden soll. Auf dem Server immer false.

Spinner über seine loading-Prop.