use-deferred-commit.ts 1.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. 'use client'
  2. import { useCallback, useEffect, useRef } from 'react'
  3. /** Delay before a pause in typing counts as having finished the value. */
  4. export const DEFAULT_COMMIT_DELAY_MS = 400
  5. /**
  6. * Runs work once a burst of typing stops.
  7. *
  8. * For fields derived from another field that updates on every keystroke. The
  9. * parts editor derives a margin from cost and price: entering a cost of 1500
  10. * against a price of 1000 passes through 1, and a margin honestly reported
  11. * from a cost of 1 reads 99900%, so the field flashes 99900, 6566.7 and 566.7
  12. * before settling. Each is arithmetically right for the digits on screen at
  13. * the time, and all three look broken.
  14. *
  15. * Waiting for a pause rather than for the field to be left keeps the update
  16. * automatic; `flush` covers leaving early, when there is nothing left to wait
  17. * for.
  18. */
  19. export function useDeferredCommit(delayMs: number = DEFAULT_COMMIT_DELAY_MS) {
  20. const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
  21. const pending = useRef<(() => void) | null>(null)
  22. const cancel = useCallback(() => {
  23. if (timer.current) clearTimeout(timer.current)
  24. timer.current = null
  25. pending.current = null
  26. }, [])
  27. const schedule = useCallback(
  28. (work: () => void) => {
  29. if (timer.current) clearTimeout(timer.current)
  30. pending.current = work
  31. timer.current = setTimeout(() => {
  32. timer.current = null
  33. const queued = pending.current
  34. pending.current = null
  35. queued?.()
  36. }, delayMs)
  37. },
  38. [delayMs]
  39. )
  40. /** Run the pending work now, if any. */
  41. const flush = useCallback(() => {
  42. const queued = pending.current
  43. cancel()
  44. queued?.()
  45. }, [cancel])
  46. // A row unmounted mid-edit must not fire a state update afterwards.
  47. useEffect(() => cancel, [cancel])
  48. return { schedule, cancel, flush }
  49. }