HextaUI

useButtonFeedback

Bir asenkron eylemi yükleme, başarı ve hata aşamalarından geçirir; hızlı isteklerde spinner'ı atlar ve hatayı siz okurken ekranda tutar.

    "use client"
    
    import * as React from "react"
    
    import { Button } from "@/components/ui/button"
    import {
      useButtonFeedback,
      type ButtonStatus,
    } from "@/hooks/use-button-feedback"
    
    function wait(ms: number) {
      return new Promise((resolve) => setTimeout(resolve, ms))
    }
    
    function RequestButton({
      ms,
      onStatus,
    }: {
      ms: number
      onStatus: (entry: string) => void
    }) {
      const { buttonProps, track } = useButtonFeedback({
        onStatusChange: (status: ButtonStatus) => onStatus(`${ms}ms: ${status}`),
      })
    
      return (
        <Button
          {...buttonProps}
          variant="outline"
          successLabel="Done"
          onClick={() => track(wait(ms))}
        >
          {ms >= 1000 ? `${ms / 1000}s` : `${ms}ms`} request
        </Button>
      )
    }
    
    export function UseButtonFeedbackFast() {
      const [log, setLog] = React.useState<string[]>([])
      const push = React.useCallback(
        (entry: string) => setLog((entries) => [...entries.slice(-3), entry]),
        []
      )
    
      return (
        <div className="flex w-full max-w-xs flex-col items-center gap-4">
          <div className="flex gap-2">
            <RequestButton ms={80} onStatus={push} />
            <RequestButton ms={1200} onStatus={push} />
          </div>
          <ol className="flex min-h-20 flex-col items-center gap-0.5 font-mono text-xs text-muted-foreground">
            {log.map((entry, index) => (
              <li key={index}>{entry}</li>
            ))}
          </ol>
        </div>
      )
    }
    pnpm dlx shadcn@latest add https://hextaui.com/r/use-button-feedback.json

    Hook'u ve bağımlı olduğu her şeyi projenize ekler.

    import { useButtonFeedback } from "@/hooks/use-button-feedback"
    const { buttonProps, track } = useButtonFeedback()
    
    <form onSubmit={(event) => {
      event.preventDefault()
      track(() => saveProfile(new FormData(event.currentTarget)))
    }}>
      …
      <Button type="submit" {...buttonProps}>Save</Button>
    </form>

    <Button feedback>, onClick bir promise döndürdüğünde bu akışı sizin için çalıştırır. İş başka bir yerde başladığında, örneğin bir formun onSubmit'inde, klavye kısayolunda veya blur'da hook'u kullanın. Durumun bir düğmeye ait olmadığı yerlerde de çalışır.

    track() bir promise veya onu döndüren bir fonksiyon alır ve status'u idle, loading, ardından success veya error üzerinden geçirip yeniden idle'a döndürür. Sakin hissettiren şey zamanlamadır.

    AdımAçıklama
    0–150msDurum idle kalır. Bu pencerede sonuçlanan bir istek spinner olmadan doğrudan success veya error olur.
    loading150ms'den itibaren gösterilir. Gösterildikten sonra en az 400ms sürer, böylece asla çakmaz.
    successVarsayılan olarak 2 saniye tutulur, sonra idle'a döner.
    errorVarsayılan olarak 4 saniye tutulur. İşaretçi düğmenin üzerindeyken veya düğme klavye odağındayken sıfırlama, onlar ayrılana kadar artı 600ms bekler.
    • Bir istek sürerken yapılan track() çağrıları yok sayılır; böylece çift tıklama veya basılı tutulan Enter tuşu isteği asla iki kez göndermez.
    • track()'e verilen ve eşzamanlı hata fırlatan bir fonksiyon, reddedilmiş bir promise gibi ele alınır.
    • reset() hemen idle'a döner. Terk edilen isteğin sonradan yaptığı her şey yok sayılır; bileşen unmount olduktan sonra sonuçlanan her şey de.
    • Hata bekletmesi yalnızca gerçek fare hover'ını ve klavye odağını sayar. Dokunmatikte hover yoktur ve bir tıklamanın odağı :focus-visible değildir; bu yüzden hiçbiri hatayı sabitlemez.

    Formlar

    onSubmit içinden track() çağırın ve buttonProps'u gönder düğmesine yayın. Hatayı görmek için @ işaretini kaldırın.

    "use client"
    
    import * as React from "react"
    
    import { Button } from "@/components/ui/button"
    import { useButtonFeedback } from "@/hooks/use-button-feedback"
    
    function wait(ms: number) {
      return new Promise<void>((resolve) => setTimeout(resolve, ms))
    }
    
    async function fail(ms: number) {
      await wait(ms)
      throw new Error("Invalid email")
    }
    
    export function ButtonForm() {
      const save = useButtonFeedback()
      const [email, setEmail] = React.useState("[email protected]")
    
      return (
        <form
          className="flex w-full max-w-sm items-center gap-2"
          onSubmit={(event) => {
            event.preventDefault()
            save.track(email.includes("@") ? wait(900) : fail(600))
          }}
        >
          <input
            aria-label="Email"
            value={email}
            onChange={(event) => setEmail(event.target.value)}
            className="h-9 min-w-0 flex-1 rounded-md border border-input bg-transparent px-3 text-sm transition-shadow outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden pointer-coarse:text-touch"
          />
          <Button
            type="submit"
            {...save.buttonProps}
            successLabel="Subscribed"
            errorLabel="Invalid email"
          >
            Subscribe
          </Button>
        </form>
      )
    }

    Düğmesiz durum

    Herhangi bir arayüzü yönlendirmek için status değerini okuyun. Bu not odağı kaybedince kaydedilir ve sonucu yanında, ekran okuyucuların duyurduğu bir role="status" bölgesinde gösterir.

    "use client"
    
    import * as React from "react"
    import { IconAlertCircle, IconCircleCheck } from "@tabler/icons-react"
    
    import { Spinner } from "@/components/ui/spinner"
    import { Switch } from "@/components/ui/switch"
    import { useButtonFeedback } from "@/hooks/use-button-feedback"
    
    function save(fail: boolean) {
      return new Promise<void>((resolve, reject) =>
        setTimeout(
          () => (fail ? reject(new Error("Network error")) : resolve()),
          900
        )
      )
    }
    
    const labels = {
      idle: "",
      loading: "Saving…",
      success: "Saved",
      error: "Couldn’t save",
    }
    
    export function UseButtonFeedbackAutosave() {
      const [fail, setFail] = React.useState(false)
      const { status, track } = useButtonFeedback({ resetAfter: 1500 })
    
      return (
        <div className="flex w-full max-w-sm flex-col gap-4">
          <textarea
            aria-label="Notes"
            rows={4}
            defaultValue="Edit me, then click outside to save."
            onBlur={() => track(() => save(fail))}
            className="w-full resize-none rounded-lg bg-muted px-3 py-2 text-sm/6 outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden pointer-coarse:text-touch"
          />
          <div className="flex items-center justify-between gap-4 text-sm">
            <label className="flex items-center gap-2 text-muted-foreground">
              <Switch checked={fail} onCheckedChange={setFail} size="sm" />
              Fail the save
            </label>
            <span
              role="status"
              className="flex h-5 items-center gap-1.5 text-muted-foreground data-[status=error]:text-destructive"
              data-status={status}
            >
              {status === "loading" ? <Spinner size="sm" /> : null}
              {status === "success" ? (
                <IconCircleCheck className="size-3.5 text-success" />
              ) : null}
              {status === "error" ? <IconAlertCircle className="size-3.5" /> : null}
              {labels[status]}
            </span>
          </div>
        </div>
      )
    }
    const { status, error, track, reset } = useButtonFeedback({
      resetAfter: { success: 1500, error: 6000 },
      onError: (error) => reportError(error),
    })

    resetAfter her iki sonuç için tek bir sayı veya her birini ayarlamak için bir nesne alır. error son reddetme nedenini tutar; böylece Button'ın hata ayrıntıları örneğindeki gibi etikette gösterebilirsiniz.

    • Her düğmeye kendi hook'unu verin. Tek bir buttonProps'u paylaşan iki düğme aynı durumu gösterir.
    • onStatusChange ve onError her zaman verdiğiniz en son fonksiyonu çağırır, bu yüzden satır içi fonksiyonlar sorun değildir.
    • track() dışındaki işi korumak için isPending() kullanın. Bir ref okur, bu yüzden bir sonraki işlemeden önce bile doğrudur.
    PropTürVarsayılan
    resetAfterBaşarı ve hatanın idle'a dönmeden önce ne kadar kaldığı.
    number | { success?: number; error?: number }{ success: 2000, error: 4000 }
    onStatusChangeHer durum değişikliğinde çağrılır.
    (status: ButtonStatus) => void–
    onErrorRet nedeniyle çağrılır.
    (error: unknown) => void–
    ÖzellikAçıklama
    track(action)Bir promise veya onu döndüren bir fonksiyon verin. Bir istek sürerken yok sayılır.
    buttonPropsstatus artı işaretçi ve odak işleyicileri. <Button> üzerine veya bu işleyicileri birleştiren herhangi bir şeye yayın.
    status"idle" | "loading" | "success" | "error"
    errorSon ret nedeni.
    reset()Hemen idle'a döner ve süren isteği yok sayar.
    isPending()Bir isteğin sürüp sürmediği.

    feedback prop'u üzerinden Button.