Skip to content

Commit a2d3eb5

Browse files
committed
fix(CDropdown): prevent unnecessary re-rendering
1 parent 90d8100 commit a2d3eb5

File tree

3 files changed

+63
-62
lines changed

3 files changed

+63
-62
lines changed

packages/coreui-react/src/components/dropdown/CDropdown.tsx

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import React, { ElementType, forwardRef, HTMLAttributes, useEffect, useRef, useState } from 'react'
1+
import React, {
2+
ElementType,
3+
forwardRef,
4+
HTMLAttributes,
5+
useCallback,
6+
useEffect,
7+
useMemo,
8+
useRef,
9+
useState,
10+
} from 'react'
211
import PropTypes from 'prop-types'
312
import classNames from 'classnames'
413
import type { Options } from '@popperjs/core'
@@ -190,59 +199,63 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
190199
ref
191200
) => {
192201
const dropdownRef = useRef<HTMLDivElement>(null)
193-
const dropdownToggleRef = useRef<HTMLElement>(null)
194202
const dropdownMenuRef = useRef<HTMLDivElement | HTMLUListElement>(null)
195203
const forkedRef = useForkedRef(ref, dropdownRef)
204+
const [dropdownToggleElement, setDropdownToggleElement] = useState<HTMLElement | null>(null)
196205
const [_visible, setVisible] = useState(visible)
197206
const { initPopper, destroyPopper } = usePopper()
198207

199-
const Component = variant === 'nav-item' ? 'li' : as
208+
const dropdownToggleRef = useCallback((node: HTMLElement | null) => {
209+
if (node) {
210+
setDropdownToggleElement(node)
211+
}
212+
}, [])
200213

201-
// Disable popper if responsive aligment is set.
202-
if (typeof alignment === 'object') {
203-
popper = false
204-
}
214+
const allowPopperUse = popper && typeof alignment !== 'object'
215+
const Component = variant === 'nav-item' ? 'li' : as
205216

206217
const contextValues = {
207218
alignment,
208219
container,
209220
dark,
210-
dropdownToggleRef,
211221
dropdownMenuRef,
212-
popper,
222+
dropdownToggleRef,
223+
popper: allowPopperUse,
213224
portal,
214225
variant,
215226
visible: _visible,
216227
setVisible,
217228
}
218229

219-
const defaultPopperConfig = {
220-
modifiers: [
221-
{
222-
name: 'offset',
223-
options: {
224-
offset: offset,
230+
const computedPopperConfig: Partial<Options> = useMemo(() => {
231+
const defaultPopperConfig = {
232+
modifiers: [
233+
{
234+
name: 'offset',
235+
options: {
236+
offset,
237+
},
225238
},
226-
},
227-
],
228-
placement: getPlacement(placement, direction, alignment, isRTL(dropdownMenuRef.current)),
229-
}
239+
],
240+
placement: getPlacement(placement, direction, alignment, isRTL(dropdownMenuRef.current)),
241+
}
230242

231-
const computedPopperConfig: Partial<Options> = {
232-
...defaultPopperConfig,
233-
...(typeof popperConfig === 'function' ? popperConfig(defaultPopperConfig) : popperConfig),
234-
}
243+
return {
244+
...defaultPopperConfig,
245+
...(typeof popperConfig === 'function' ? popperConfig(defaultPopperConfig) : popperConfig),
246+
}
247+
}, [offset, placement, direction, alignment, popperConfig])
235248

236249
useEffect(() => {
237250
setVisible(visible)
238251
}, [visible])
239252

240253
useEffect(() => {
241-
const toggleElement = dropdownToggleRef.current
254+
const toggleElement = dropdownToggleElement
242255
const menuElement = dropdownMenuRef.current
243256

244257
if (_visible && toggleElement && menuElement) {
245-
if (popper) {
258+
if (allowPopperUse) {
246259
initPopper(toggleElement, menuElement, computedPopperConfig)
247260
}
248261

@@ -257,7 +270,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
257270
}
258271

259272
return () => {
260-
if (popper) {
273+
if (allowPopperUse) {
261274
destroyPopper()
262275
}
263276

@@ -269,14 +282,7 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
269282

270283
onHide?.()
271284
}
272-
}, [
273-
computedPopperConfig,
274-
destroyPopper,
275-
dropdownMenuRef,
276-
dropdownToggleRef,
277-
initPopper,
278-
_visible,
279-
])
285+
}, [dropdownToggleElement, _visible])
280286

281287
const handleKeydown = (event: KeyboardEvent) => {
282288
if (
@@ -304,11 +310,11 @@ export const CDropdown: PolymorphicRefForwardingComponent<'div', CDropdownProps>
304310
}
305311

306312
const handleMouseUp = (event: Event) => {
307-
if (!dropdownToggleRef.current || !dropdownMenuRef.current) {
313+
if (!dropdownToggleElement || !dropdownMenuRef.current) {
308314
return
309315
}
310316

311-
if (dropdownToggleRef.current.contains(event.target as HTMLElement)) {
317+
if (dropdownToggleElement.contains(event.target as HTMLElement)) {
312318
return
313319
}
314320

packages/coreui-react/src/components/dropdown/CDropdownContext.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ export interface CDropdownContextProps {
55
alignment?: Alignments
66
container?: DocumentFragment | Element | (() => DocumentFragment | Element | null) | null
77
dark?: boolean
8-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9-
dropdownToggleRef: RefObject<any | null>
108
dropdownMenuRef: RefObject<HTMLDivElement | HTMLUListElement | null>
9+
dropdownToggleRef: (node: HTMLElement | null) => void
1110
setVisible: React.Dispatch<React.SetStateAction<boolean | undefined>>
1211
popper?: boolean
1312
portal?: boolean

packages/coreui-react/src/components/dropdown/CDropdownToggle.tsx

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -74,36 +74,32 @@ export const CDropdownToggle: FC<CDropdownToggleProps> = ({
7474
...(!rest.disabled && { ...triggers }),
7575
}
7676

77-
const Toggler = () => {
78-
if (custom && React.isValidElement(children)) {
79-
return (
80-
<>
81-
{React.cloneElement(children as React.ReactElement<any>, {
82-
'aria-expanded': visible,
83-
...(!rest.disabled && { ...triggers }),
84-
ref: dropdownToggleRef,
85-
})}
86-
</>
87-
)
88-
}
89-
90-
if (variant === 'nav-item' && navLink) {
91-
return (
92-
<a href="#" {...togglerProps} role="button" {...rest} ref={dropdownToggleRef}>
93-
{children}
94-
</a>
95-
)
96-
}
77+
if (custom && React.isValidElement(children)) {
78+
return (
79+
<>
80+
{React.cloneElement(children as React.ReactElement<any>, {
81+
'aria-expanded': visible,
82+
...(!rest.disabled && { ...triggers }),
83+
ref: dropdownToggleRef,
84+
})}
85+
</>
86+
)
87+
}
9788

89+
if (variant === 'nav-item' && navLink) {
9890
return (
99-
<CButton {...togglerProps} tabIndex={0} {...rest} ref={dropdownToggleRef}>
91+
<a href="#" {...togglerProps} role="button" {...rest} ref={dropdownToggleRef}>
10092
{children}
101-
{split && <span className="visually-hidden">Toggle Dropdown</span>}
102-
</CButton>
93+
</a>
10394
)
10495
}
10596

106-
return <Toggler />
97+
return (
98+
<CButton {...togglerProps} tabIndex={0} {...rest} ref={dropdownToggleRef}>
99+
{children}
100+
{split && <span className="visually-hidden">Toggle Dropdown</span>}
101+
</CButton>
102+
)
107103
}
108104

109105
CDropdownToggle.propTypes = {

0 commit comments

Comments
 (0)