Have something to say?

Tell HeroUI Pro how they could make the product more useful to you.

NumberField: Runaway onChange loop when spam-tapping increment/decrement buttons (iOS)

# NumberField: Runaway onChange loop when spam-tapping increment/decrement buttons (iOS) **Category:** Bug **Severity:** High **Component:** `NumberField` (heroui-native-pro) --- ## Description When using `NumberField` in controlled mode (`value` + `onChange`), rapidly tapping the increment (+) or decrement (-) button on iOS causes `onChange` to fire in an infinite loop. The value runs away to `maxValue` or `minValue` and does not stop even after tapping stops. This is also reproducible in the official **HeroUI Native iOS app**. ## Environment - heroui-native-pro: 1.0.0-beta.4 (also reproduced on beta.3) - heroui-native: ^1.0.3 - expo: ~55.0.26 - react-native: 0.83 - Platform: iOS ## Steps to Reproduce 1. Render a `NumberField` in controlled mode with `value`, `onChange`, `step={50}`, `minValue={0}`, `maxValue={10000}` 2. On iOS, rapidly tap (spam-press) the increment (+) or decrement (-) button 3. Observe the value β€” it keeps incrementing/decrementing on its own and does not stop ## Expected Behavior Each tap increments/decrements by `step` once. Rapid taps should increment once per tap. The value should stop changing when tapping stops. ## Actual Behavior After rapid taps, `onChange` continues firing in a loop. The value runs away to `maxValue`/`minValue` without stopping. ## Root Cause (from source inspection) Two issues in `useLongPressRepeat`: ### 1. Timer leak on rapid press-in `onPressIn` sets `timerRef.current` to a new `setTimeout` **without clearing the previous one**. During spam-tapping, if two `onPressIn` events fire before `onPressOut` clears them (iOS touch coalescing), the first timeout is leaked β€” its ref was overwritten, so `clear()` can never cancel it. That orphaned timeout spawns a `setInterval` that runs indefinitely. ```js // useLongPressRepeat.js β€” current code const onPressIn = useCallback(() => { actionRef.current(); timerRef.current = setTimeout(() => { // ← overwrites without clearing intervalRef.current = setInterval(() => { // ← also overwrites without clearing actionRef.current(); }, interval); }, delay); }, [delay, interval]); ``` ### 2. Stale closure in increment/decrement The `increment`/`decrement` callbacks in `NumberFieldRoot` close over `numberValue` via `useCallback`. When the orphaned interval fires, `actionRef.current` may hold a closure with a stale `numberValue`, causing each tick to re-compute the same step and emit duplicate `onChange` calls. ## Suggested Fix In `useLongPressRepeat`, clear existing timers before starting new ones: ```js const onPressIn = useCallback(() => { if (isDisabledRef.current) return; clear(); // ← clear any existing timer/interval first actionRef.current(); timerRef.current = setTimeout(() => { intervalRef.current = setInterval(() => { actionRef.current(); }, interval); }, delay); }, [delay, interval, clear]); ``` Additionally, consider using a ref for `numberValue` inside `increment`/`decrement` instead of closing over it, so interval ticks always read the latest value. ## Minimal Repro ```tsx import { useCallback, useMemo, useState } from "react"; import { View } from "react-native"; import { Text } from "heroui-native"; import { NumberField } from "heroui-native-pro"; export default function NumberFieldRepro() { const [amount, setAmount] = useState(0); const [changeCount, setChangeCount] = useState(0); const formatOptions = useMemo ( () => ({ style: "currency", currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0 }), [], ); const handleChange = useCallback((newValue: number) => { if (isNaN(newValue)) return; console.log(`[NumberField onChange] newValue=${newValue}`); setChangeCount((c) => c + 1); setAmount(newValue); }, []); return (

Stefan Kudla about 2 months ago

πŸ› Bug Reports

The theme sync functionality in the theme builder for heroui pro is not working properly

And the difference between the normal theme builder seen in the heroui docs is that the hero ui theme builder gets every single css variable: /* HeroUI Theme Customization Add this to your global.css after importing @heroui/styles Only includes variables users need to customize @see https://heroui.com/docs/react/getting-started/theming */ :root, .light, .default, [data-theme="light"], [data-theme="default"] { /* Theme Colors (Light Mode) */ --accent: oklch(0 0 0); --accent-foreground: oklch(99.11% 0 0); --background: oklch(97.02% 0.0000 0.00); --border: oklch(90.00% 0.0000 0.00); --danger: oklch(0.573 0.2249 21.97); --danger-foreground: oklch(98% 0.0200 21.97); --default: oklch(94.00% 0.0000 0.00); --default-foreground: oklch(21.03% 0.0059 0.00); --field-background: oklch(100.00% 0.0000 0.00); --field-border: transparent; --field-foreground: oklch(21.03% 0.0000 0.00); --field-placeholder: oklch(55.17% 0.0000 0.00); --focus: oklch(0 0 0); --foreground: oklch(21.03% 0.0000 0.00); --muted: oklch(55.17% 0.0000 0.00); --overlay: oklch(100.00% 0.0000 0.00); --overlay-foreground: oklch(21.03% 0.0000 0.00); --scrollbar: oklch(87.10% 0.0000 0.00); --segment: oklch(100.00% 0.0000 0.00); --segment-foreground: oklch(21.03% 0.0000 0.00); --separator: oklch(92.00% 0.0000 0.00); --success: oklch(0.6277 0.1604 153.06); --success-foreground: oklch(98% 0.0160 153.06); --surface: oklch(100.00% 0.0000 0.00); --surface-foreground: oklch(21.03% 0.0000 0.00); --surface-secondary: oklch(95.24% 0.0000 0.00); --surface-secondary-foreground: oklch(21.03% 0.0000 0.00); --surface-tertiary: oklch(93.73% 0.0000 0.00); --surface-tertiary-foreground: oklch(21.03% 0.0000 0.00); --warning: oklch(0.8446 0.1525 80.6); --warning-foreground: oklch(15% 0.0457 80.60); --accent-soft-foreground: oklch(0 0 0); /* Border Radius */ --radius: 0.25rem; --field-radius: 0.25rem; /* Font Family / / Make sure to load Inter font in your app */ --font-sans: var(--font-inter); } .dark, [data-theme="dark"] { color-scheme: dark; /* Theme Colors (Dark Mode) */ --accent: oklch(0.9848 0 0); --accent-foreground: oklch(15% 0.0000 0.00); --background: oklch(12.00% 0.0000 0.00); --border: oklch(28.00% 0.0000 0.00); --danger: oklch(0.7044 0.1872 23.19); --danger-foreground: oklch(15% 0.0500 23.19); --default: oklch(27.40% 0.0000 0.00); --default-foreground: oklch(99.11% 0 0); --field-background: oklch(21.03% 0.0000 0.00); --field-border: transparent; --field-foreground: oklch(99.11% 0.0000 0.00); --field-placeholder: oklch(70.50% 0.0000 0.00); --focus: oklch(0.9848 0 0); --foreground: oklch(99.11% 0.0000 0.00); --muted: oklch(70.50% 0.0000 0.00); --overlay: oklch(21.03% 0.0000 0.00); --overlay-foreground: oklch(99.11% 0.0000 0.00); --scrollbar: oklch(70.50% 0.0000 0.00); --segment: oklch(39.64% 0.0000 0.00); --segment-foreground: oklch(99.11% 0.0000 0.00); --separator: oklch(25.00% 0.0000 0.00); --success: oklch(0.6514 0.1321 156.22); --success-foreground: oklch(15% 0.0396 156.22); --surface: oklch(21.03% 0.0000 0.00); --surface-foreground: oklch(99.11% 0.0000 0.00); --surface-secondary: oklch(25.70% 0.0000 0.00); --surface-secondary-foreground: oklch(99.11% 0.0000 0.00); --surface-tertiary: oklch(27.21% 0.0000 0.00); --surface-tertiary-foreground: oklch(99.11% 0.0000 0.00); --warning: oklch(0.8803 0.1348 86.06); --warning-foreground: oklch(15% 0.0404 86.06); --accent-soft-foreground: oklch(0.9848 0 0); } This is the pro version: /* HeroUI Pro Theme Add this to your globals.css after importing @heroui/styles @see https://heroui.com/docs/react/getting-started/theming */ /* Font: Inter Load this font in your app: */ @import "@heroui-pro

Juan Carlos Tremols S about 2 months ago

πŸ› Bug Reports