Skip to content

Commit 52bff1c

Browse files
fix(swipe-cell): clear timers on component unmount (#837)
* Initial plan * fix(swipe-cell): clear timers on component unmount to prevent memory leaks Co-authored-by: liweijie0812 <[email protected]> * fix(swipe-cell): remove completed timers from tracking array to prevent memory accumulation Co-authored-by: liweijie0812 <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: liweijie0812 <[email protected]>
1 parent c4050d2 commit 52bff1c

File tree

2 files changed

+61
-10
lines changed

2 files changed

+61
-10
lines changed

src/swipe-cell/SwipeCell.tsx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { forwardRef, useImperativeHandle, useRef, useMemo, useState } from 'react';
1+
import React, { forwardRef, useImperativeHandle, useRef, useMemo, useState, useEffect } from 'react';
22
import { isArray, isBoolean } from 'lodash-es';
33
import classNames from 'classnames';
44
import { useClickAway } from 'ahooks';
@@ -31,6 +31,7 @@ export const syncOpenedState = (
3131
getOpenedSide: (opened: SwipeCellProps['opened']) => SideType | undefined,
3232
expand: (side: SideType) => void,
3333
close: () => void,
34+
setTimer: (callback: () => void, delay: number) => void,
3435
) => {
3536
if (!rootRef.current) return;
3637

@@ -39,7 +40,7 @@ export const syncOpenedState = (
3940
if (side === 'left' || side === 'right') {
4041
// 初始化 expanded,等待 dom 加载完,获取 left/right 宽度后无动画设置展开状态
4142
// 防止 left/right 为列表时,获取真实宽度有误
42-
setTimeout(() => {
43+
setTimer(() => {
4344
expand(side);
4445
}, 100);
4546
} else {
@@ -54,12 +55,36 @@ const SwipeCell = forwardRef<SwipeCellRef, SwipeCellProps>((originProps, ref) =>
5455
const rootRef = useRef<HTMLDivElement>(null);
5556
const leftRef = useRef<HTMLDivElement>(null);
5657
const rightRef = useRef<HTMLDivElement>(null);
58+
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
5759
const [curSure, setSure] = useState<{
5860
content: Sure;
5961
width: number;
6062
transform: string;
6163
}>({ content: '', width: 0, transform: 'none' });
6264

65+
// Helper function to set timers that are tracked for cleanup
66+
const setTimer = (callback: () => void, delay: number) => {
67+
const timerId = setTimeout(() => {
68+
// Remove completed timer from tracking array
69+
const index = timersRef.current.indexOf(timerId);
70+
if (index > -1) {
71+
timersRef.current.splice(index, 1);
72+
}
73+
callback();
74+
}, delay);
75+
timersRef.current.push(timerId);
76+
return timerId;
77+
};
78+
79+
// Cleanup all timers on unmount
80+
useEffect(
81+
() => () => {
82+
timersRef.current.forEach((timerId) => clearTimeout(timerId));
83+
timersRef.current = [];
84+
},
85+
[],
86+
);
87+
6388
const getOpenedSide = (opened) => {
6489
if (isBoolean(opened)) {
6590
if (rightRef.current) {
@@ -108,7 +133,7 @@ const SwipeCell = forwardRef<SwipeCellRef, SwipeCellProps>((originProps, ref) =>
108133
setX(0);
109134
onChange();
110135
if (curSure.content) {
111-
setTimeout(() => {
136+
setTimer(() => {
112137
setSure({
113138
content: '',
114139
width: 0,
@@ -152,9 +177,9 @@ const SwipeCell = forwardRef<SwipeCellRef, SwipeCellProps>((originProps, ref) =>
152177
} else {
153178
close();
154179
}
155-
setTimeout(() => {
180+
setTimer(() => {
156181
ctx.dragging = false;
157-
});
182+
}, 0);
158183
} else {
159184
setX(offsetX);
160185
}
@@ -178,7 +203,7 @@ const SwipeCell = forwardRef<SwipeCellRef, SwipeCellProps>((originProps, ref) =>
178203
}));
179204

180205
useLayoutEffect(() => {
181-
syncOpenedState(rootRef, opened, getOpenedSide, expand, close);
206+
syncOpenedState(rootRef, opened, getOpenedSide, expand, close, setTimer);
182207
// 可以保证expand,close正常执行
183208
// eslint-disable-next-line react-hooks/exhaustive-deps
184209
}, [opened, rootRef.current]);
@@ -200,12 +225,12 @@ const SwipeCell = forwardRef<SwipeCellRef, SwipeCellProps>((originProps, ref) =>
200225
width: getSideOffsetX(side),
201226
transform: side === 'left' ? 'translateX(-100%)' : 'translateX(100%)',
202227
});
203-
setTimeout(() => {
228+
setTimer(() => {
204229
setSure((current) => ({
205230
...current,
206231
transform: 'none',
207232
}));
208-
});
233+
}, 0);
209234
return;
210235
}
211236

src/swipe-cell/__tests__/swipe-cell.test.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -758,7 +758,8 @@ describe('SwipeCell', () => {
758758
vi.useFakeTimers();
759759
const expand = vi.fn();
760760
const close = vi.fn();
761-
syncOpenedState({ current: null } as any, [true, false], () => 'left', expand, close);
761+
const setTimer = vi.fn((cb, delay) => setTimeout(cb, delay));
762+
syncOpenedState({ current: null } as any, [true, false], () => 'left', expand, close, setTimer);
762763
expect(expand).not.toHaveBeenCalled();
763764
expect(close).not.toHaveBeenCalled();
764765
vi.useRealTimers();
@@ -768,11 +769,36 @@ describe('SwipeCell', () => {
768769
vi.useFakeTimers();
769770
const expand = vi.fn();
770771
const close = vi.fn();
771-
syncOpenedState({ current: {} } as any, [true, false], () => 'right', expand, close);
772+
const setTimer = vi.fn((cb, delay) => setTimeout(cb, delay));
773+
syncOpenedState({ current: {} } as any, [true, false], () => 'right', expand, close, setTimer);
772774
expect(expand).not.toHaveBeenCalled();
773775
vi.advanceTimersByTime(120);
774776
expect(expand).toHaveBeenCalledWith('right');
775777
expect(close).not.toHaveBeenCalled();
776778
vi.useRealTimers();
777779
});
780+
781+
it('clears timers on unmount', async () => {
782+
vi.useFakeTimers();
783+
const { unmount, container } = render(<SwipeCell right={rightActions} content={<div>内容</div>} opened />);
784+
const rightEl = container.querySelector('.t-swipe-cell__right') as HTMLElement;
785+
Object.defineProperty(rightEl, 'clientWidth', { value: 100, configurable: true });
786+
787+
// Trigger timer via expand
788+
await act(async () => {
789+
vi.advanceTimersByTime(50);
790+
});
791+
792+
// Unmount should clear timers without errors
793+
unmount();
794+
795+
// Run remaining timers - should not cause any errors since they were cleared
796+
await act(async () => {
797+
vi.advanceTimersByTime(200);
798+
});
799+
800+
// No error means timers were properly cleaned up
801+
expect(true).toBe(true);
802+
vi.useRealTimers();
803+
});
778804
});

0 commit comments

Comments
 (0)