Skip to content

Commit 7e59dc8

Browse files
fix(cascader): 修复外部值变化时同步问题 (#7046)
* fix(cascader): 修复外部值变化时同步问题 - 移除visible条件判断,确保值同步 - 添加测试用例验证值同步逻辑 * feat(cascader): 使用 useDeepCompareEffect 优化性能 - 替换 useEffect 为 useDeepCompareEffect 避免不必要的重新渲染 - 添加测试用例验证父组件重渲染时保持选中状态 * refactor(cascader): 优化组件导入和状态管理 - 调整导入顺序和分组 - 替换useDeepCompareEffect为useEffect - 简化状态管理逻辑
1 parent b0de7c6 commit 7e59dc8

2 files changed

Lines changed: 103 additions & 17 deletions

File tree

src/components/cascader/cascader.tsx

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
import React, {
2-
useState,
3-
useEffect,
42
ReactNode,
53
forwardRef,
4+
useEffect,
65
useImperativeHandle,
6+
useState,
77
} from 'react'
8-
import Popup, { PopupProps } from '../popup'
9-
import {
8+
import type { FieldNamesType } from '../../hooks'
9+
import { useFieldNames } from '../../hooks'
10+
import { NativeProps, withNativeProps } from '../../utils/native-props'
11+
import { usePropsValue } from '../../utils/use-props-value'
12+
import { mergeProps } from '../../utils/with-default-props'
13+
import CascaderView, {
14+
CascaderOption,
1015
CascaderValue,
1116
CascaderValueExtend,
12-
CascaderOption,
1317
} from '../cascader-view'
14-
import { mergeProps } from '../../utils/with-default-props'
15-
import { NativeProps, withNativeProps } from '../../utils/native-props'
16-
import { usePropsValue } from '../../utils/use-props-value'
17-
import CascaderView from '../cascader-view'
18-
import { useConfig } from '../config-provider'
1918
import { useCascaderValueExtend } from '../cascader-view/use-cascader-value-extend'
20-
import { useFieldNames } from '../../hooks'
21-
import type { FieldNamesType } from '../../hooks'
19+
import { useConfig } from '../config-provider'
20+
import Popup, { PopupProps } from '../popup'
2221

2322
const classPrefix = `adm-cascader`
2423

@@ -120,9 +119,7 @@ export const Cascader = forwardRef<CascaderRef, CascaderProps>((p, ref) => {
120119
const [innerValue, setInnerValue] = useState<CascaderValue[]>(value)
121120

122121
useEffect(() => {
123-
if (!visible) {
124-
setInnerValue(value)
125-
}
122+
setInnerValue(value)
126123
}, [visible, value])
127124

128125
const cascaderElement = withNativeProps(

src/components/cascader/tests/cascader.test.tsx

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState } from 'react'
2-
import { fireEvent, render, testA11y, waitFor } from 'testing'
3-
import Cascader from '../'
2+
import { fireEvent, render, screen, testA11y, waitFor } from 'testing'
3+
import Cascader, { CascaderValue } from '../'
44
import { options } from '../demos/data'
55

66
describe('Cascader', () => {
@@ -52,6 +52,95 @@ describe('Cascader', () => {
5252
expect(onConfirm.mock.calls[0][0]).toEqual(['浙江', '杭州'])
5353
})
5454

55+
test('should sync value when visible and external value changes', async () => {
56+
const App = () => {
57+
const [visible, setVisible] = useState(false)
58+
const [value, setValue] = useState<CascaderValue[]>([])
59+
60+
return (
61+
<>
62+
<button onClick={() => setVisible(true)}>Open</button>
63+
<button onClick={() => setValue(['安徽', '合肥'])}>Set Value</button>
64+
<Cascader
65+
options={options}
66+
visible={visible}
67+
value={value}
68+
onConfirm={val => {
69+
setValue(val)
70+
setVisible(false)
71+
}}
72+
onClose={() => setVisible(false)}
73+
onCancel={() => setVisible(false)}
74+
/>
75+
</>
76+
)
77+
}
78+
79+
render(<App />)
80+
81+
fireEvent.click(screen.getByText('Open'))
82+
await waitFor(() => {
83+
expect(screen.getByText('浙江')).toBeInTheDocument()
84+
})
85+
86+
// While the popup is open, change the external value
87+
fireEvent.click(screen.getByText('Set Value'))
88+
89+
// The cascader should reflect the new value (合肥 should appear in both tab and list)
90+
await waitFor(() => {
91+
const matches = screen.getAllByText('合肥')
92+
expect(matches.length).toBeGreaterThanOrEqual(1)
93+
})
94+
})
95+
96+
test('should preserve draft selection when parent re-renders with same value', async () => {
97+
const App = () => {
98+
const [visible, setVisible] = useState(false)
99+
const [value, setValue] = useState<CascaderValue[]>([])
100+
const [, setTick] = useState(0)
101+
102+
return (
103+
<>
104+
<button onClick={() => setVisible(true)}>Open</button>
105+
{/* Trigger a parent re-render without changing value content */}
106+
<button onClick={() => setTick(t => t + 1)}>Rerender</button>
107+
<Cascader
108+
options={options}
109+
visible={visible}
110+
value={value}
111+
onConfirm={val => {
112+
setValue(val)
113+
setVisible(false)
114+
}}
115+
onClose={() => setVisible(false)}
116+
onCancel={() => setVisible(false)}
117+
/>
118+
</>
119+
)
120+
}
121+
122+
render(<App />)
123+
124+
fireEvent.click(screen.getByText('Open'))
125+
await waitFor(() => {
126+
expect(screen.getByText('浙江')).toBeInTheDocument()
127+
})
128+
129+
// User selects 浙江 in the cascader (draft, not confirmed)
130+
fireEvent.click(screen.getByText('浙江'))
131+
132+
// Wait for the selection to take effect
133+
await waitFor(() => {
134+
expect(screen.getAllByText('浙江').length).toBeGreaterThanOrEqual(2)
135+
})
136+
137+
// Parent re-renders with the same value content ([])
138+
fireEvent.click(screen.getByText('Rerender'))
139+
140+
// The draft selection should be preserved — 浙江 should still appear as a selected tab
141+
expect(screen.getAllByText('浙江').length).toBeGreaterThanOrEqual(2)
142+
})
143+
55144
test('use in an imperative way', async () => {
56145
const fn = jest.fn()
57146
const onClick = async () => {

0 commit comments

Comments
 (0)