From 61cec6e56858b9d06dce3ce488857cb62461890e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 20:02:29 +0000 Subject: [PATCH] Fix dice intermittently not animating on rollAll The roll animation was triggered imperatively by resetting the die's className and forcing a reflow (className='die' + offsetWidth + classList.add). This restart is unreliable when React's commit timing shifts, so on rapid / overlapping rollAll presses some dice would not restart their animation and appeared to skip rolling (while totals still completed). Drive the roll through React state instead and bump a rollKey so the animated element remounts on every roll, which restarts the CSS keyframe animation deterministically. Removes the dieRef/reflow hack entirely. Verified in jsdom: across full-wait, fast-overlapping, and near-completion press sequences, all dice remount and receive a roll class on every press. Needs visual confirmation on the demo in a real browser. https://claude.ai/code/session_01WZxB2pKeMMacz4juCo19DK --- lib/Die.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/Die.tsx b/lib/Die.tsx index a97597c..8b30593 100644 --- a/lib/Die.tsx +++ b/lib/Die.tsx @@ -40,7 +40,6 @@ const Die = forwardRef( }: DieProps, ref ): JSX.Element => { - const dieRef = useRef(null) const timeoutRef = useRef | null>(null) useEffect(() => { @@ -60,6 +59,11 @@ const Die = forwardRef( const clampedDefault = Math.min(Math.max(defaultRoll || 6, 1), 6) const [dieValue, setDieValue] = useState(clampedDefault) const [hasRolled, setHasRolled] = useState(false) + // Bumped on every roll so React remounts the animated element, which + // restarts the CSS keyframe animation reliably (replaces the old + // className-reset + forced-reflow hack that could fail to restart on + // rapid/overlapping rolls). + const [rollKey, setRollKey] = useState(0) // Only d6 faces are rendered; clamp to 1-6 regardless of sides prop const getRandomInt = () => { @@ -68,15 +72,15 @@ const Die = forwardRef( } const rollDie = (value?: number) => { - dieRef.current && (dieRef.current.className = `die`) - void dieRef.current?.offsetWidth const rawRoll = disableRandom ? dieValue : value || getRandomInt() const roll = Math.min(Math.max(rawRoll, 1), 6) - dieRef.current?.classList.add(`roll${roll}`) + // Drive the roll through state and remount the animated node (rollKey) + // so the CSS animation always restarts, even on rapid/overlapping rolls. + setDieValue(roll) + setHasRolled(true) + setRollKey((k) => k + 1) if (timeoutRef.current !== null) clearTimeout(timeoutRef.current) timeoutRef.current = setTimeout(() => { - setHasRolled(true) - setDieValue(roll) onRollDone(roll) timeoutRef.current = null }, rollTime * 1000) @@ -172,8 +176,8 @@ const Die = forwardRef( style={containerStyle} >