Skip to content

Commit e13441d

Browse files
committed
perf: use listener cleanups and proper hook dependencies
1 parent 63f9472 commit e13441d

File tree

3 files changed

+110
-34
lines changed

3 files changed

+110
-34
lines changed

packages/dragswag/src/react/drag-overlay.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ type DragOverlayContextType = {
77
setDragElementPosition: (position: { top: number; left: number }) => void
88
}
99

10+
// Create context with safe default values
11+
const defaultContextValue: DragOverlayContextType = {
12+
dragElement: null,
13+
setDragElement: () => {},
14+
setDragElementPosition: () => {},
15+
}
16+
17+
const DragOverlayContext = createContext<DragOverlayContextType>(defaultContextValue)
18+
1019
interface DragOverlayProviderProps {
1120
children: React.ReactNode
1221
style?: React.CSSProperties
@@ -43,8 +52,6 @@ function DragOverlay({ ref, style, children: dragElement, ...props }: React.Comp
4352
)
4453
}
4554

46-
const DragOverlayContext = createContext<DragOverlayContextType | null>(null)
47-
4855
export function DragOverlayProvider({ children, ...rest }: DragOverlayProviderProps) {
4956
const [dragElement, setDragElement] = useState<ReactElement | null>(null)
5057
const dragWrapperRef = useRef<HTMLDivElement>(null)
@@ -65,17 +72,11 @@ export function DragOverlayProvider({ children, ...rest }: DragOverlayProviderPr
6572
)
6673
}
6774

68-
const noContext: DragOverlayContextType = {
69-
dragElement: null,
70-
setDragElement: () => {},
71-
setDragElementPosition: () => {},
72-
}
73-
7475
export function useDragOverlayElement(): DragOverlayContextType {
7576
const context = useContext(DragOverlayContext)
7677
if (!context) {
7778
console.warn('useDragOverlayElement must be used within a DragOverlayProvider')
78-
return noContext
79+
return defaultContextValue
7980
}
8081
return context
8182
}

packages/dragswag/src/react/use-draggable.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export function useDraggable(config: DraggableConfig) {
1919
data: null as any,
2020
currentDragComponent: null as React.ReactElement | null,
2121
elementDimensions: { width: 0, height: 0 },
22+
componentCleanup: null as (() => void) | null,
23+
dragComponentCleanup: null as (() => void) | null,
2224
})
2325

2426
refs.current.config = config
@@ -127,18 +129,36 @@ export function useDraggable(config: DraggableConfig) {
127129
plugins: config.plugins,
128130
}
129131

130-
const dragSource = useMemo(() => createDragSource(trueConfig), [])
132+
// Create dragSource only once but update its config when it changes
133+
const dragSource = useMemo(() => {
134+
const source = createDragSource(trueConfig)
135+
return source
136+
}, [])
131137

132-
dragSource.setConfig(trueConfig)
138+
// Update config when it changes
139+
useEffect(() => {
140+
dragSource.setConfig(trueConfig)
141+
}, [
142+
dragSource,
143+
config.disabled,
144+
config.kind,
145+
config.data,
146+
config.shouldDrag,
147+
config.mouseConfig,
148+
config.plugins,
149+
config.onDragStart,
150+
config.onDragMove,
151+
config.onDragEnd,
152+
])
133153

134154
const componentRef = useCallback(
135155
(element: HTMLElement | null) => {
136156
const current = refs.current
137157

138158
if (element) {
139159
current.element = element
140-
141-
dragSource.listen(element)
160+
// Store the cleanup function
161+
current.componentCleanup = dragSource.listen(element)
142162
}
143163

144164
const ref = current.originalRef
@@ -154,8 +174,11 @@ export function useDraggable(config: DraggableConfig) {
154174

155175
const dragComponentRef = useCallback(
156176
(element: HTMLElement | null) => {
177+
const current = refs.current
178+
157179
if (element) {
158-
dragSource.listen(element)
180+
// Store the cleanup function
181+
current.dragComponentCleanup = dragSource.listen(element)
159182
}
160183
},
161184
[dragSource],
@@ -218,6 +241,19 @@ export function useDraggable(config: DraggableConfig) {
218241
}
219242
}, [isDragging, setDragElement])
220243

244+
// Clean up all listeners when component unmounts
245+
useEffect(() => {
246+
return () => {
247+
const current = refs.current
248+
if (current.componentCleanup) {
249+
current.componentCleanup()
250+
}
251+
if (current.dragComponentCleanup) {
252+
current.dragComponentCleanup()
253+
}
254+
}
255+
}, [])
256+
221257
return {
222258
draggable,
223259
isDragging,

packages/dragswag/src/react/use-droppable.tsx

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useMemo, useRef, useState } from 'react'
1+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { type DragSourceType, type DropTargetConfig, createDropTarget } from '../core'
33
import type { DroppableConfig, Kind } from './types'
44
import { getDropTargets } from './utils'
@@ -12,11 +12,20 @@ type HoveredData = {
1212

1313
export function useDroppable(config: DroppableConfig) {
1414
const [hovered, setHovered] = useState<HoveredData | null>(null)
15+
const cleanupRef = useRef<(() => void) | null>(null)
16+
const previousAcceptsRef = useRef(config.accepts)
1517

1618
let { accepts } = config
1719

1820
const trueAccepts = Array.isArray(accepts) || typeof accepts === 'function' ? accepts : [accepts]
1921

22+
// Detect if accepts has changed
23+
const acceptsChanged = useMemo(() => {
24+
const hasChanged = previousAcceptsRef.current !== accepts
25+
previousAcceptsRef.current = accepts
26+
return hasChanged
27+
}, [accepts])
28+
2029
const trueConfig: DropTargetConfig<any> = {
2130
disabled: config.disabled,
2231
accepts: trueAccepts as unknown as DragSourceType<any>[],
@@ -74,35 +83,65 @@ export function useDroppable(config: DroppableConfig) {
7483
},
7584
}
7685

86+
// Create dropTarget only once
7787
const dropTarget = useMemo(() => createDropTarget(trueConfig), [])
7888

79-
dropTarget.setConfig(trueConfig)
89+
// Update config when it changes
90+
useEffect(() => {
91+
dropTarget.setConfig(trueConfig)
92+
}, [
93+
dropTarget,
94+
config.disabled,
95+
config.data,
96+
config.onDragIn,
97+
config.onDragOut,
98+
config.onDragMove,
99+
config.onDrop,
100+
acceptsChanged, // Use the memoized value that indicates if accepts changed
101+
])
80102

81103
const originalRef = useRef(null as any)
82104

83-
const dropComponentRef = useCallback((element: HTMLElement | null) => {
84-
if (element) {
85-
dropTarget.listen(element)
86-
}
105+
const dropComponentRef = useCallback(
106+
(element: HTMLElement | null) => {
107+
if (element) {
108+
// Store the cleanup function
109+
cleanupRef.current = dropTarget.listen(element)
110+
}
87111

88-
const ref = originalRef.current
112+
const ref = originalRef.current
89113

90-
if (typeof ref === 'function') {
91-
ref(element)
92-
} else if (ref && ref.hasOwnProperty('current')) {
93-
ref.current = element
94-
}
95-
}, [])
114+
if (typeof ref === 'function') {
115+
ref(element)
116+
} else if (ref && ref.hasOwnProperty('current')) {
117+
ref.current = element
118+
}
119+
},
120+
[dropTarget],
121+
)
96122

97-
const droppable = useCallback((child: React.ReactElement<React.ComponentPropsWithRef<any>> | null) => {
98-
if (!child) {
99-
return null
100-
}
123+
const droppable = useCallback(
124+
(child: React.ReactElement<React.ComponentPropsWithRef<any>> | null) => {
125+
if (!child) {
126+
return null
127+
}
101128

102-
// @ts-ignore React 16-19+ refs compatibility.
103-
originalRef.current = child.props?.ref ?? child.ref
129+
// @ts-ignore React 16-19+ refs compatibility.
130+
originalRef.current = child.props?.ref ?? child.ref
104131

105-
return React.cloneElement(child, { ref: dropComponentRef })
132+
return React.cloneElement(child, { ref: dropComponentRef })
133+
},
134+
[dropComponentRef],
135+
)
136+
137+
// Clean up all listeners when component unmounts
138+
useEffect(() => {
139+
return () => {
140+
if (cleanupRef.current) {
141+
cleanupRef.current()
142+
cleanupRef.current = null
143+
}
144+
}
106145
}, [])
107146

108147
return {

0 commit comments

Comments
 (0)