From 4cdbb261fc371e3561cff74ef5a5831e8db3388d Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 31 May 2026 04:05:11 -0700 Subject: [PATCH] perf: move update card timer cleanup to ref (#4093) --- src/renderer/src/components/UpdateCard.tsx | 31 +++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/renderer/src/components/UpdateCard.tsx b/src/renderer/src/components/UpdateCard.tsx index 791955cfb8f..6f542e33805 100644 --- a/src/renderer/src/components/UpdateCard.tsx +++ b/src/renderer/src/components/UpdateCard.tsx @@ -1,7 +1,7 @@ /* eslint-disable max-lines -- Why: the update card owns the full updater lifecycle in one renderer surface. Keeping the state machine and its presentation variants together avoids scattering tightly coupled update behavior across multiple files. */ -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { usePrefersReducedMotion } from '@/hooks/usePrefersReducedMotion' import { useAppStore } from '../store' import { Card } from './ui/card' @@ -200,17 +200,29 @@ export function UpdateCard() { // ── Prefers-reduced-motion ────────────────────────────────────────── const prefersReducedMotion = usePrefersReducedMotion() - useEffect(() => { - return () => { - if (dismissAnimationTimerRef.current !== null) { - window.clearTimeout(dismissAnimationTimerRef.current) - } - if (collapseAnimationTimerRef.current !== null) { - window.clearTimeout(collapseAnimationTimerRef.current) - } + const clearAnimationTimers = useCallback(() => { + if (dismissAnimationTimerRef.current !== null) { + window.clearTimeout(dismissAnimationTimerRef.current) + dismissAnimationTimerRef.current = null + } + if (collapseAnimationTimerRef.current !== null) { + window.clearTimeout(collapseAnimationTimerRef.current) + collapseAnimationTimerRef.current = null } }, []) + const cardRootRef = useCallback( + (node: HTMLDivElement | null) => { + if (node !== null) { + return + } + // Why: exit timers are owned by the visible update-card surface, so + // stale callbacks should be cancelled as soon as that surface unmounts. + clearAnimationTimers() + }, + [clearAnimationTimers] + ) + // ── Visibility gates ────────────────────────────────────────────── const isUserInitiated = 'userInitiated' in status && status.userInitiated @@ -542,6 +554,7 @@ export function UpdateCard() { return (