diff --git a/.changeset/dnd-monitor-refactor.md b/.changeset/dnd-monitor-refactor.md new file mode 100644 index 00000000..c7607b00 --- /dev/null +++ b/.changeset/dnd-monitor-refactor.md @@ -0,0 +1,7 @@ +--- +'@dnd-kit/core': minor +--- + +The `useDndMonitor()` hook has been refactored to be synchronously invoked at the same time as the events dispatched by `` (such as `onDragStart`, `onDragOver`, `onDragEnd`). + +The new refactor uses the subscribe/notify pattern and no longer causes re-renders in consuming components of `useDndMonitor()` when events are dispatched. diff --git a/packages/core/src/components/Accessibility/Accessibility.tsx b/packages/core/src/components/Accessibility/Accessibility.tsx index 1ef9de84..d15b0533 100644 --- a/packages/core/src/components/Accessibility/Accessibility.tsx +++ b/packages/core/src/components/Accessibility/Accessibility.tsx @@ -3,8 +3,8 @@ import {createPortal} from 'react-dom'; import {useUniqueId} from '@dnd-kit/utilities'; import {HiddenText, LiveRegion, useAnnouncement} from '@dnd-kit/accessibility'; -import {DndMonitorArguments, useDndMonitor} from '../../hooks/monitor'; import type {UniqueIdentifier} from '../../types'; +import {DndMonitorListener, useDndMonitor} from '../DndMonitor'; import type {Announcements, ScreenReaderInstructions} from './types'; import { @@ -34,7 +34,7 @@ export function Accessibility({ }, []); useDndMonitor( - useMemo( + useMemo( () => ({ onDragStart({active}) { announce(announcements.onDragStart(active.id)); diff --git a/packages/core/src/components/DndContext/DndContext.tsx b/packages/core/src/components/DndContext/DndContext.tsx index 7fefde0d..a241dfdd 100644 --- a/packages/core/src/components/DndContext/DndContext.tsx +++ b/packages/core/src/components/DndContext/DndContext.tsx @@ -28,7 +28,7 @@ import { getInitialState, reducer, } from '../../store'; -import {DndMonitorContext, DndMonitorState} from '../../hooks/monitor'; +import {DndMonitorContext, useDndMonitorProvider} from '../DndMonitor'; import { useAutoScroller, useCachedNode, @@ -144,10 +144,10 @@ export const DndContext = memo(function DndContext({ }: Props) { const store = useReducer(reducer, undefined, getInitialState); const [state, dispatch] = store; - const [monitorState, setMonitorState] = useState(() => ({ - type: null, - event: null, - })); + const [ + dispatchMonitorEvent, + registerMonitorListener, + ] = useDndMonitorProvider(); const [status, setStatus] = useState(Status.Uninitialized); const isInitialized = status === Status.Initialized; const { @@ -375,7 +375,7 @@ export const DndContext = memo(function DndContext({ initialCoordinates, active: id, }); - setMonitorState({type: Action.DragStart, event}); + dispatchMonitorEvent({type: 'onDragStart', event}); }); }, onMove(coordinates) { @@ -432,16 +432,14 @@ export const DndContext = memo(function DndContext({ setActiveSensor(null); setActivatorEvent(null); - if (event) { - setMonitorState({type, event}); - } + const eventName = + type === Action.DragEnd ? 'onDragEnd' : 'onDragCancel'; if (event) { - const {onDragCancel, onDragEnd} = latestProps.current; - const handler = - type === Action.DragEnd ? onDragEnd : onDragCancel; + const handler = latestProps.current[eventName]; handler?.(event); + dispatchMonitorEvent({type: eventName, event}); } }); }; @@ -527,8 +525,10 @@ export const DndContext = memo(function DndContext({ over, }; - setMonitorState({type: Action.DragMove, event}); - onDragMove?.(event); + unstable_batchedUpdates(() => { + onDragMove?.(event); + dispatchMonitorEvent({type: 'onDragMove', event}); + }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [scrollAdjustedTranslate.x, scrollAdjustedTranslate.y] @@ -577,8 +577,8 @@ export const DndContext = memo(function DndContext({ unstable_batchedUpdates(() => { setOver(over); - setMonitorState({type: Action.DragOver, event}); onDragOver?.(event); + dispatchMonitorEvent({type: 'onDragOver', event}); }); }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -701,7 +701,7 @@ export const DndContext = memo(function DndContext({ ]); return ( - + diff --git a/packages/core/src/components/DndMonitor/context.ts b/packages/core/src/components/DndMonitor/context.ts new file mode 100644 index 00000000..343b5335 --- /dev/null +++ b/packages/core/src/components/DndMonitor/context.ts @@ -0,0 +1,5 @@ +import {createContext} from 'react'; + +import type {RegisterListener} from './types'; + +export const DndMonitorContext = createContext(null); diff --git a/packages/core/src/components/DndMonitor/index.ts b/packages/core/src/components/DndMonitor/index.ts new file mode 100644 index 00000000..ee765c26 --- /dev/null +++ b/packages/core/src/components/DndMonitor/index.ts @@ -0,0 +1,4 @@ +export {DndMonitorContext} from './context'; +export type {DndMonitorListener, DndMonitorEvent} from './types'; +export {useDndMonitor} from './useDndMonitor'; +export {useDndMonitorProvider} from './useDndMonitorProvider'; diff --git a/packages/core/src/components/DndMonitor/types.ts b/packages/core/src/components/DndMonitor/types.ts new file mode 100644 index 00000000..0d7c2655 --- /dev/null +++ b/packages/core/src/components/DndMonitor/types.ts @@ -0,0 +1,31 @@ +import type { + DragStartEvent, + DragCancelEvent, + DragEndEvent, + DragMoveEvent, + DragOverEvent, +} from '../../types'; + +export interface DndMonitorListener { + onDragStart?(event: DragStartEvent): void; + onDragMove?(event: DragMoveEvent): void; + onDragOver?(event: DragOverEvent): void; + onDragEnd?(event: DragEndEvent): void; + onDragCancel?(event: DragCancelEvent): void; +} + +export interface DndMonitorEvent { + type: keyof DndMonitorListener; + event: + | DragStartEvent + | DragMoveEvent + | DragOverEvent + | DragEndEvent + | DragCancelEvent; +} + +export type UnregisterListener = () => void; + +export type RegisterListener = ( + listener: DndMonitorListener +) => UnregisterListener; diff --git a/packages/core/src/components/DndMonitor/useDndMonitor.ts b/packages/core/src/components/DndMonitor/useDndMonitor.ts new file mode 100644 index 00000000..cd23c891 --- /dev/null +++ b/packages/core/src/components/DndMonitor/useDndMonitor.ts @@ -0,0 +1,20 @@ +import {useContext, useEffect} from 'react'; + +import {DndMonitorContext} from './context'; +import type {DndMonitorListener} from './types'; + +export function useDndMonitor(listener: DndMonitorListener) { + const registerListener = useContext(DndMonitorContext); + + useEffect(() => { + if (!registerListener) { + throw new Error( + 'useDndMonitor must be used within a children of ' + ); + } + + const unsubscribe = registerListener(listener); + + return unsubscribe; + }, [listener, registerListener]); +} diff --git a/packages/core/src/components/DndMonitor/useDndMonitorProvider.tsx b/packages/core/src/components/DndMonitor/useDndMonitorProvider.tsx new file mode 100644 index 00000000..044fe1db --- /dev/null +++ b/packages/core/src/components/DndMonitor/useDndMonitorProvider.tsx @@ -0,0 +1,24 @@ +import {useCallback, useState} from 'react'; + +import type {DndMonitorListener, DndMonitorEvent} from './types'; + +export function useDndMonitorProvider() { + const [listeners] = useState(() => new Set()); + + const registerListener = useCallback( + (listener) => { + listeners.add(listener); + return () => listeners.delete(listener); + }, + [listeners] + ); + + const dispatch = useCallback( + ({type, event}: DndMonitorEvent) => { + listeners.forEach((listener) => listener[type]?.(event as any)); + }, + [listeners] + ); + + return [dispatch, registerListener] as const; +} diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index 5c4b730c..c06536c1 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -10,6 +10,8 @@ export type { DraggableMeasuring, MeasuringConfiguration, } from './DndContext'; +export {useDndMonitor} from './DndMonitor'; +export type {DndMonitorListener} from './DndMonitor'; export { DragOverlay, defaultDropAnimation, diff --git a/packages/core/src/hooks/index.ts b/packages/core/src/hooks/index.ts index e7d5cc27..4ee9dda6 100644 --- a/packages/core/src/hooks/index.ts +++ b/packages/core/src/hooks/index.ts @@ -1,5 +1,3 @@ -export {useDndMonitor} from './monitor'; -export type {DndMonitorArguments} from './monitor'; export {useDraggable} from './useDraggable'; export type { DraggableAttributes, diff --git a/packages/core/src/hooks/monitor/index.ts b/packages/core/src/hooks/monitor/index.ts deleted file mode 100644 index 1b715475..00000000 --- a/packages/core/src/hooks/monitor/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export {DndMonitorContext, useDndMonitor} from './useDndMonitor'; -export type { - Arguments as DndMonitorArguments, - DndMonitorState, -} from './useDndMonitor'; diff --git a/packages/core/src/hooks/monitor/useDndMonitor.ts b/packages/core/src/hooks/monitor/useDndMonitor.ts deleted file mode 100644 index 048ed8fd..00000000 --- a/packages/core/src/hooks/monitor/useDndMonitor.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {createContext, useContext, useEffect, useRef} from 'react'; - -import {Action} from '../../store'; -import type { - DragStartEvent, - DragCancelEvent, - DragEndEvent, - DragMoveEvent, - DragOverEvent, -} from '../../types'; - -export interface Arguments { - onDragStart?(event: DragStartEvent): void; - onDragMove?(event: DragMoveEvent): void; - onDragOver?(event: DragOverEvent): void; - onDragEnd?(event: DragEndEvent): void; - onDragCancel?(event: DragCancelEvent): void; -} - -export interface DndMonitorState { - type: Action | null; - event: - | null - | DragStartEvent - | DragMoveEvent - | DragOverEvent - | DragEndEvent - | DragCancelEvent; -} - -export const DndMonitorContext = createContext({ - type: null, - event: null, -}); - -export function useDndMonitor({ - onDragStart, - onDragMove, - onDragOver, - onDragEnd, - onDragCancel, -}: Arguments) { - const monitorState = useContext(DndMonitorContext); - const previousMonitorState = useRef(monitorState); - - useEffect(() => { - if (monitorState !== previousMonitorState.current) { - const {type, event} = monitorState; - - switch (type) { - case Action.DragStart: - onDragStart?.(event as DragStartEvent); - break; - case Action.DragMove: - onDragMove?.(event as DragMoveEvent); - break; - case Action.DragOver: - onDragOver?.(event as DragOverEvent); - break; - case Action.DragCancel: - onDragCancel?.(event as DragCancelEvent); - break; - case Action.DragEnd: - onDragEnd?.(event as DragEndEvent); - break; - } - - previousMonitorState.current = monitorState; - } - }, [ - monitorState, - onDragStart, - onDragMove, - onDragOver, - onDragEnd, - onDragCancel, - ]); -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b2f5d2c6..304f8b1a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,11 +5,14 @@ export { defaultScreenReaderInstructions, defaultDropAnimation, defaultDropAnimationSideEffects, + useDndMonitor, } from './components'; export type { Announcements, CancelDrop, DndContextProps, + DndMonitorListener, + DndMonitorListener as DndMonitorArguments, DragOverlayProps, DropAnimation, DropAnimationFunction, @@ -28,12 +31,10 @@ export { TraversalOrder, useDraggable, useDndContext, - useDndMonitor, useDroppable, } from './hooks'; export type { AutoScrollOptions, - DndMonitorArguments, DraggableAttributes, DraggableSyntheticListeners, DroppableMeasuring,