From 98aa31c1626833494777f207ca4f592ff1c791e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:44:43 +0000 Subject: [PATCH 1/3] Initial plan From baae51a3a9c5e0b02e8216296f5e969551d9a75e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:53:19 +0000 Subject: [PATCH 2/3] fix(swipe-cell): clear timers on component unmount to prevent memory leaks Co-authored-by: liweijie0812 <10710889+liweijie0812@users.noreply.github.com> --- src/swipe-cell/SwipeCell.tsx | 34 +++++++++++++++----- src/swipe-cell/__tests__/swipe-cell.test.tsx | 30 +++++++++++++++-- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/swipe-cell/SwipeCell.tsx b/src/swipe-cell/SwipeCell.tsx index f6b24361..ca8245f2 100644 --- a/src/swipe-cell/SwipeCell.tsx +++ b/src/swipe-cell/SwipeCell.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, useImperativeHandle, useRef, useMemo, useState } from 'react'; +import React, { forwardRef, useImperativeHandle, useRef, useMemo, useState, useEffect } from 'react'; import { isArray, isBoolean } from 'lodash-es'; import classNames from 'classnames'; import { useClickAway } from 'ahooks'; @@ -31,6 +31,7 @@ export const syncOpenedState = ( getOpenedSide: (opened: SwipeCellProps['opened']) => SideType | undefined, expand: (side: SideType) => void, close: () => void, + setTimer: (callback: () => void, delay: number) => void, ) => { if (!rootRef.current) return; @@ -39,7 +40,7 @@ export const syncOpenedState = ( if (side === 'left' || side === 'right') { // 初始化 expanded,等待 dom 加载完,获取 left/right 宽度后无动画设置展开状态 // 防止 left/right 为列表时,获取真实宽度有误 - setTimeout(() => { + setTimer(() => { expand(side); }, 100); } else { @@ -54,12 +55,29 @@ const SwipeCell = forwardRef((originProps, ref) => const rootRef = useRef(null); const leftRef = useRef(null); const rightRef = useRef(null); + const timersRef = useRef[]>([]); const [curSure, setSure] = useState<{ content: Sure; width: number; transform: string; }>({ content: '', width: 0, transform: 'none' }); + // Helper function to set timers that are tracked for cleanup + const setTimer = (callback: () => void, delay: number) => { + const timerId = setTimeout(callback, delay); + timersRef.current.push(timerId); + return timerId; + }; + + // Cleanup all timers on unmount + useEffect( + () => () => { + timersRef.current.forEach((timerId) => clearTimeout(timerId)); + timersRef.current = []; + }, + [], + ); + const getOpenedSide = (opened) => { if (isBoolean(opened)) { if (rightRef.current) { @@ -108,7 +126,7 @@ const SwipeCell = forwardRef((originProps, ref) => setX(0); onChange(); if (curSure.content) { - setTimeout(() => { + setTimer(() => { setSure({ content: '', width: 0, @@ -152,9 +170,9 @@ const SwipeCell = forwardRef((originProps, ref) => } else { close(); } - setTimeout(() => { + setTimer(() => { ctx.dragging = false; - }); + }, 0); } else { setX(offsetX); } @@ -178,7 +196,7 @@ const SwipeCell = forwardRef((originProps, ref) => })); useLayoutEffect(() => { - syncOpenedState(rootRef, opened, getOpenedSide, expand, close); + syncOpenedState(rootRef, opened, getOpenedSide, expand, close, setTimer); // 可以保证expand,close正常执行 // eslint-disable-next-line react-hooks/exhaustive-deps }, [opened, rootRef.current]); @@ -200,12 +218,12 @@ const SwipeCell = forwardRef((originProps, ref) => width: getSideOffsetX(side), transform: side === 'left' ? 'translateX(-100%)' : 'translateX(100%)', }); - setTimeout(() => { + setTimer(() => { setSure((current) => ({ ...current, transform: 'none', })); - }); + }, 0); return; } diff --git a/src/swipe-cell/__tests__/swipe-cell.test.tsx b/src/swipe-cell/__tests__/swipe-cell.test.tsx index 31e51184..81d1e64c 100644 --- a/src/swipe-cell/__tests__/swipe-cell.test.tsx +++ b/src/swipe-cell/__tests__/swipe-cell.test.tsx @@ -758,7 +758,8 @@ describe('SwipeCell', () => { vi.useFakeTimers(); const expand = vi.fn(); const close = vi.fn(); - syncOpenedState({ current: null } as any, [true, false], () => 'left', expand, close); + const setTimer = vi.fn((cb, delay) => setTimeout(cb, delay)); + syncOpenedState({ current: null } as any, [true, false], () => 'left', expand, close, setTimer); expect(expand).not.toHaveBeenCalled(); expect(close).not.toHaveBeenCalled(); vi.useRealTimers(); @@ -768,11 +769,36 @@ describe('SwipeCell', () => { vi.useFakeTimers(); const expand = vi.fn(); const close = vi.fn(); - syncOpenedState({ current: {} } as any, [true, false], () => 'right', expand, close); + const setTimer = vi.fn((cb, delay) => setTimeout(cb, delay)); + syncOpenedState({ current: {} } as any, [true, false], () => 'right', expand, close, setTimer); expect(expand).not.toHaveBeenCalled(); vi.advanceTimersByTime(120); expect(expand).toHaveBeenCalledWith('right'); expect(close).not.toHaveBeenCalled(); vi.useRealTimers(); }); + + it('clears timers on unmount', async () => { + vi.useFakeTimers(); + const { unmount, container } = render(内容} opened />); + const rightEl = container.querySelector('.t-swipe-cell__right') as HTMLElement; + Object.defineProperty(rightEl, 'clientWidth', { value: 100, configurable: true }); + + // Trigger timer via expand + await act(async () => { + vi.advanceTimersByTime(50); + }); + + // Unmount should clear timers without errors + unmount(); + + // Run remaining timers - should not cause any errors since they were cleared + await act(async () => { + vi.advanceTimersByTime(200); + }); + + // No error means timers were properly cleaned up + expect(true).toBe(true); + vi.useRealTimers(); + }); }); From 74936c33435ff6cb21dd37b14ba34d9431a5f666 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:55:17 +0000 Subject: [PATCH 3/3] fix(swipe-cell): remove completed timers from tracking array to prevent memory accumulation Co-authored-by: liweijie0812 <10710889+liweijie0812@users.noreply.github.com> --- src/swipe-cell/SwipeCell.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/swipe-cell/SwipeCell.tsx b/src/swipe-cell/SwipeCell.tsx index ca8245f2..1816d570 100644 --- a/src/swipe-cell/SwipeCell.tsx +++ b/src/swipe-cell/SwipeCell.tsx @@ -64,7 +64,14 @@ const SwipeCell = forwardRef((originProps, ref) => // Helper function to set timers that are tracked for cleanup const setTimer = (callback: () => void, delay: number) => { - const timerId = setTimeout(callback, delay); + const timerId = setTimeout(() => { + // Remove completed timer from tracking array + const index = timersRef.current.indexOf(timerId); + if (index > -1) { + timersRef.current.splice(index, 1); + } + callback(); + }, delay); timersRef.current.push(timerId); return timerId; };