diff --git a/src/swipe-cell/SwipeCell.tsx b/src/swipe-cell/SwipeCell.tsx index f6b24361..1816d570 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,36 @@ 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(() => { + // 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; + }; + + // 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 +133,7 @@ const SwipeCell = forwardRef((originProps, ref) => setX(0); onChange(); if (curSure.content) { - setTimeout(() => { + setTimer(() => { setSure({ content: '', width: 0, @@ -152,9 +177,9 @@ const SwipeCell = forwardRef((originProps, ref) => } else { close(); } - setTimeout(() => { + setTimer(() => { ctx.dragging = false; - }); + }, 0); } else { setX(offsetX); } @@ -178,7 +203,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 +225,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(); + }); });