HextaUI

useDelayedLoading

N'affiche un état de chargement que lorsque le travail est vraiment lent, puis le garde assez longtemps pour qu'il ne scintille jamais.

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

Ajoute le hook et tout ce dont il dépend à votre projet.

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

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

Passez l'indicateur de chargement brut et rendez à partir du booléen renvoyé. La plupart des requêtes sur une connexion chaude se terminent en moins de 150ms. Afficher un spinner pour celles-ci est pire que de ne rien afficher : il clignote une image ou deux et passe pour un bug, non pour une progression.

Le hook applique deux règles. Il attend delay avant d'afficher quoi que ce soit, si bien qu'un travail qui se termine plus tôt n'affiche jamais d'état de chargement. Une fois l'indicateur visible, il reste au moins minDuration, pour ne pas apparaître et disparaître en quelques images.

Le travail prendDescription
80msRien n'est affiché.
250msAffiché à 150ms et maintenu jusqu'à 550ms, le minimum de 400ms.
900msAffiché à 150ms et masqué dès que le travail se termine.

Le minimum de 400ms est assez long pour être perçu comme un état délibéré et assez court pour ne ralentir personne.

  • Si loading repasse à true alors que l'indicateur est encore visible, il reste simplement visible. Il n'y a pas de masquage puis de ré-affichage.
  • Les minuteurs sont effacés quand les entrées changent ou que le composant se démonte : rien ne met donc à jour l'état une fois le composant disparu.
  • Côté serveur et pendant le premier rendu, il renvoie false : il n'ajoute donc jamais de désaccord d'hydratation.

Skeletons

Les skeletons remplacent du contenu : un éclair est donc encore plus brutal qu'avec un spinner. Ici, le premier chargement est lent et affiche le skeleton. Les suivants viennent d'un cache et n'en affichent jamais.

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

Augmentez delay pour les indicateurs qui couvrent une grande partie de l'écran, comme les skeletons ou les overlays. Baissez-le vers 0 pour les actions où toute attente doit être signalée, comme un paiement. Gardez minDuration au-dessus d'environ 300ms.

  • <Spinner loading={...} /> et <Button loading> utilisent déjà ces timings. Prenez le hook quand vous rendez autre chose.
  • Gardez la place que prendra l'indicateur, comme le font les exemples, pour que la mise en page ne bouge pas quand il apparaît.
  • Associez-le à un aria-busy ou à un message de statut. Le hook ne décide que de ce qui s'affiche visuellement.
PropTypePar défaut
loadingIndique si le travail est en cours à l'instant.
boolean–
options.delayMillisecondes d'attente avant d'afficher l'état de chargement.
number150
options.minDurationMillisecondes minimales pendant lesquelles l'état de chargement reste visible une fois affiché.
number400
Valeur de retourDescription
booleanIndique s'il faut afficher l'état de chargement. Toujours false côté serveur.

Spinner via sa prop loading.