Skip to content

Commit 0cf3d90

Browse files
huangkairanhuangkairan
andauthored
fix: SubMenu in React18 sync problem (#537)
Co-authored-by: huangkairan <[email protected]>
1 parent d98ed07 commit 0cf3d90

File tree

3 files changed

+59
-8
lines changed

3 files changed

+59
-8
lines changed

src/Menu.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as React from 'react';
2+
import { flushSync } from 'react-dom';
23
import type { CSSMotionProps } from 'rc-motion';
34
import classNames from 'classnames';
45
import shallowEqual from 'shallowequal';
@@ -271,13 +272,18 @@ const Menu = React.forwardRef<MenuRef, MenuProps>((props, ref) => {
271272
});
272273

273274
const triggerOpenKeys = (keys: string[]) => {
274-
setMergedOpenKeys(keys);
275+
// Prevent React18 auto batch since trigger openKeys on same time
276+
// which makes mergedOpenKeys closure problem
277+
flushSync(() => {
278+
setMergedOpenKeys(keys);
279+
});
275280
onOpenChange?.(keys);
276281
};
277282

278283
// >>>>> Cache & Reset open keys when inlineCollapsed changed
279-
const [inlineCacheOpenKeys, setInlineCacheOpenKeys] =
280-
React.useState(mergedOpenKeys);
284+
const [inlineCacheOpenKeys, setInlineCacheOpenKeys] = React.useState(
285+
mergedOpenKeys,
286+
);
281287

282288
const isInlineMode = mergedMode === 'inline';
283289

@@ -329,10 +335,9 @@ const Menu = React.forwardRef<MenuRef, MenuProps>((props, ref) => {
329335
[registerPath, unregisterPath],
330336
);
331337

332-
const pathUserContext = React.useMemo(
333-
() => ({ isSubPathKey }),
334-
[isSubPathKey],
335-
);
338+
const pathUserContext = React.useMemo(() => ({ isSubPathKey }), [
339+
isSubPathKey,
340+
]);
336341

337342
React.useEffect(() => {
338343
refreshOverflowKeys(

tests/React18.spec.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable no-undef */
22
import React from 'react';
3-
import { act, render } from '@testing-library/react';
3+
import { act, fireEvent, render } from '@testing-library/react';
4+
import { sleep } from './util';
45
import Menu, { MenuItem, SubMenu } from '../src';
56
import type { MenuProps } from '../src';
67

@@ -55,5 +56,39 @@ describe('React18', () => {
5556
.querySelector('.rc-menu-submenu-title').textContent,
5657
).toEqual('submenu1');
5758
});
59+
60+
it('prevent React 18 auto batch', async () => {
61+
const handleOpenChange = jest.fn();
62+
const { container } = render(
63+
<Menu onOpenChange={handleOpenChange}>
64+
<SubMenu title="s1">
65+
<MenuItem key="1">1</MenuItem>
66+
</SubMenu>
67+
<SubMenu title="s2">
68+
<MenuItem key="2">2</MenuItem>
69+
</SubMenu>
70+
</Menu>,
71+
);
72+
73+
// Enter
74+
fireEvent.mouseEnter(container.querySelector('.rc-menu-submenu-title'));
75+
runAllTimer();
76+
expect(container.querySelector('.rc-menu-submenu-open')).toBeTruthy();
77+
// Leave
78+
fireEvent.mouseLeave(container.querySelector('.rc-menu-submenu-title'));
79+
act(() => {
80+
jest.runAllTimers();
81+
});
82+
expect(container.querySelector('.rc-menu-submenu-open')).toBeFalsy();
83+
await act(async () => {
84+
await sleep();
85+
});
86+
// Enter
87+
fireEvent.mouseEnter(
88+
container.querySelectorAll('.rc-menu-submenu-title')[1],
89+
);
90+
jest.runAllTimers();
91+
expect(container.querySelector('.rc-menu-submenu-open')).toBeTruthy();
92+
});
5893
});
5994
/* eslint-enable */

tests/util.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { act } from '@testing-library/react';
12
export function isActive(container: HTMLElement, index: number, active = true) {
23
const checker = expect(container.querySelectorAll('li.rc-menu-item')[index]);
34

@@ -11,3 +12,13 @@ export function isActive(container: HTMLElement, index: number, active = true) {
1112
export function last(elements: NodeListOf<Element>) {
1213
return elements[elements.length - 1];
1314
}
15+
16+
const globalTimeout = global.setTimeout;
17+
18+
export const sleep = async (timeout = 0) => {
19+
await act(async () => {
20+
await new Promise(resolve => {
21+
globalTimeout(resolve, timeout);
22+
});
23+
});
24+
};

0 commit comments

Comments
 (0)