Skip to content

Commit 4edfc30

Browse files
authored
fix(hooks): check state chage via deep compare (#1558)
* fix(hooks): check state chage via deep compare * fix function * update prev state from props * remove the props update * fix the test
1 parent a8f2699 commit 4edfc30

File tree

8 files changed

+95
-7
lines changed

8 files changed

+95
-7
lines changed

docusaurus/pages/useMultipleCombobox.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export default function DropdownMultipleCombobox() {
125125
style={{padding: '4px', cursor: 'pointer'}}
126126
onClick={e => {
127127
e.stopPropagation()
128-
removeSelectedItem(null)
128+
removeSelectedItem(selectedItemForRender)
129129
}}
130130
>
131131
✕

src/hooks/__tests__/utils.test.js

+34
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getDefaultValue,
66
useMouseAndTouchTracker,
77
getItemAndIndex,
8+
isDropdownsStateEqual,
89
} from '../utils'
910

1011
describe('utils', () => {
@@ -152,4 +153,37 @@ describe('utils', () => {
152153
})
153154
})
154155
})
156+
157+
describe('isDropdownsStateEqual', () => {
158+
test('is true when each property is equal', () => {
159+
const selectedItem = 'hello'
160+
const prevState = {
161+
highlightedIndex: 2,
162+
isOpen: true,
163+
selectedItem,
164+
inputValue: selectedItem,
165+
}
166+
const newState = {
167+
...prevState,
168+
}
169+
170+
expect(isDropdownsStateEqual(prevState, newState)).toBe(true)
171+
})
172+
173+
test('is false when at least one property is not equal', () => {
174+
const selectedItem = {value: 'hello'}
175+
const prevState = {
176+
highlightedIndex: 2,
177+
isOpen: true,
178+
selectedItem,
179+
inputValue: selectedItem,
180+
}
181+
const newState = {
182+
...prevState,
183+
selectedItem: {...selectedItem},
184+
}
185+
186+
expect(isDropdownsStateEqual(prevState, newState)).toBe(false)
187+
})
188+
})
155189
})

src/hooks/useCombobox/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
useElementIds,
1212
getItemAndIndex,
1313
getInitialValue,
14+
isDropdownsStateEqual
1415
} from '../utils'
1516
import {
1617
getInitialState,
@@ -43,6 +44,7 @@ function useCombobox(userProps = {}) {
4344
downshiftUseComboboxReducer,
4445
props,
4546
getInitialState,
47+
isDropdownsStateEqual
4648
)
4749
const {isOpen, highlightedIndex, selectedItem, inputValue} = state
4850

src/hooks/useCombobox/utils.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,16 @@ const propTypes = {
5858
* @param {Function} reducer Reducer function from downshift.
5959
* @param {Object} props The hook props, also passed to createInitialState.
6060
* @param {Function} createInitialState Function that returns the initial state.
61+
* @param {Function} isStateEqual Function that checks if a previous state is equal to the next.
6162
* @returns {Array} An array with the state and an action dispatcher.
6263
*/
63-
export function useControlledReducer(reducer, props, createInitialState) {
64+
export function useControlledReducer(reducer, props, createInitialState, isStateEqual) {
6465
const previousSelectedItemRef = useRef()
6566
const [state, dispatch] = useEnhancedReducer(
6667
reducer,
6768
props,
6869
createInitialState,
70+
isStateEqual
6971
)
7072

7173
// ToDo: if needed, make same approach as selectedItemChanged from Downshift.

src/hooks/useMultipleSelection/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
defaultProps,
1515
isKeyDownOperationPermitted,
1616
validatePropTypes,
17+
isStateEqual
1718
} from './utils'
1819
import downshiftMultipleSelectionReducer from './reducer'
1920
import * as stateChangeTypes from './stateChangeTypes'
@@ -40,6 +41,7 @@ function useMultipleSelection(userProps = {}) {
4041
downshiftMultipleSelectionReducer,
4142
props,
4243
getInitialState,
44+
isStateEqual
4345
)
4446
const {activeIndex, selectedItems} = state
4547

src/hooks/useMultipleSelection/utils.js

+16
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,21 @@ function getA11yRemovalMessage(selectionParameters) {
9595
return `${itemToStringLocal(removedSelectedItem)} has been removed.`
9696
}
9797

98+
/**
99+
* Check if a state is equal for taglist, by comparing active index and selected items.
100+
* Used by useSelect and useCombobox.
101+
*
102+
* @param {Object} prevState
103+
* @param {Object} newState
104+
* @returns {boolean} Wheather the states are deeply equal.
105+
*/
106+
function isStateEqual(prevState, newState) {
107+
return (
108+
prevState.selectedItems === newState.selectedItems &&
109+
prevState.activeIndex === newState.activeIndex
110+
)
111+
}
112+
98113
const propTypes = {
99114
...commonPropTypes,
100115
selectedItems: PropTypes.array,
@@ -133,4 +148,5 @@ export {
133148
getDefaultValue,
134149
getInitialState,
135150
isKeyDownOperationPermitted,
151+
isStateEqual,
136152
}

src/hooks/useSelect/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
useMouseAndTouchTracker,
1313
getItemAndIndex,
1414
getInitialValue,
15+
isDropdownsStateEqual,
1516
} from '../utils'
1617
import {
1718
callAllEventHandlers,
@@ -46,9 +47,9 @@ function useSelect(userProps = {}) {
4647
downshiftSelectReducer,
4748
props,
4849
getInitialState,
50+
isDropdownsStateEqual,
4951
)
5052
const {isOpen, highlightedIndex, selectedItem, inputValue} = state
51-
5253
// Element efs.
5354
const toggleButtonRef = useRef(null)
5455
const menuRef = useRef(null)

src/hooks/utils.js

+35-4
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,10 @@ function useLatestRef(val) {
189189
* @param {Function} reducer Reducer function from downshift.
190190
* @param {Object} props The hook props, also passed to createInitialState.
191191
* @param {Function} createInitialState Function that returns the initial state.
192+
* @param {Function} isStateEqual Function that checks if a previous state is equal to the next.
192193
* @returns {Array} An array with the state and an action dispatcher.
193194
*/
194-
function useEnhancedReducer(reducer, props, createInitialState) {
195+
function useEnhancedReducer(reducer, props, createInitialState, isStateEqual) {
195196
const prevStateRef = useRef()
196197
const actionRef = useRef()
197198
const enhancedReducer = useCallback(
@@ -219,7 +220,12 @@ function useEnhancedReducer(reducer, props, createInitialState) {
219220
const action = actionRef.current
220221

221222
useEffect(() => {
222-
if (action && prevStateRef.current && prevStateRef.current !== state) {
223+
const shouldCallOnChangeProps =
224+
action &&
225+
prevStateRef.current &&
226+
!isStateEqual(prevStateRef.current, state)
227+
228+
if (shouldCallOnChangeProps) {
223229
callOnChangeProps(
224230
action,
225231
getState(prevStateRef.current, action.props),
@@ -228,7 +234,7 @@ function useEnhancedReducer(reducer, props, createInitialState) {
228234
}
229235

230236
prevStateRef.current = state
231-
}, [state, props, action])
237+
}, [state, action, isStateEqual])
232238

233239
return [state, dispatchWithProps]
234240
}
@@ -240,13 +246,20 @@ function useEnhancedReducer(reducer, props, createInitialState) {
240246
* @param {Function} reducer Reducer function from downshift.
241247
* @param {Object} props The hook props, also passed to createInitialState.
242248
* @param {Function} createInitialState Function that returns the initial state.
249+
* @param {Function} isStateEqual Function that checks if a previous state is equal to the next.
243250
* @returns {Array} An array with the state and an action dispatcher.
244251
*/
245-
function useControlledReducer(reducer, props, createInitialState) {
252+
function useControlledReducer(
253+
reducer,
254+
props,
255+
createInitialState,
256+
isStateEqual,
257+
) {
246258
const [state, dispatch] = useEnhancedReducer(
247259
reducer,
248260
props,
249261
createInitialState,
262+
isStateEqual,
250263
)
251264

252265
return [getState(state, props), dispatch]
@@ -585,6 +598,23 @@ function getChangesOnSelection(props, highlightedIndex, inputValue = true) {
585598
}
586599
}
587600

601+
/**
602+
* Check if a state is equal for dropdowns, by comparing isOpen, inputValue, highlightedIndex and selected item.
603+
* Used by useSelect and useCombobox.
604+
*
605+
* @param {Object} prevState
606+
* @param {Object} newState
607+
* @returns {boolean} Wheather the states are deeply equal.
608+
*/
609+
function isDropdownsStateEqual(prevState, newState) {
610+
return (
611+
prevState.isOpen === newState.isOpen &&
612+
prevState.inputValue === newState.inputValue &&
613+
prevState.highlightedIndex === newState.highlightedIndex &&
614+
prevState.selectedItem === newState.selectedItem
615+
)
616+
}
617+
588618
// Shared between all exports.
589619
const commonPropTypes = {
590620
environment: PropTypes.shape({
@@ -646,6 +676,7 @@ export {
646676
getItemAndIndex,
647677
useElementIds,
648678
getChangesOnSelection,
679+
isDropdownsStateEqual,
649680
commonDropdownPropTypes,
650681
commonPropTypes,
651682
}

0 commit comments

Comments
 (0)