From e0d80f59c733b3adcf1fc89d29aa80257e7edd98 Mon Sep 17 00:00:00 2001 From: Clauderic Demers Date: Thu, 20 Jun 2024 22:43:27 -0400 Subject: [PATCH] Refactor lifecycle --- .changeset/refactor-lifecycle.md | 7 ++ .../src/core/entities/draggable/draggable.ts | 11 +- .../src/core/entities/droppable/droppable.ts | 4 +- .../src/core/entities/entity/entity.ts | 16 +-- packages/abstract/src/core/manager/manager.ts | 1 + .../src/core/entities/draggable/draggable.ts | 5 +- .../src/core/entities/droppable/droppable.ts | 13 ++- packages/dom/src/sortable/sortable.ts | 35 ++++-- packages/helpers/src/move.ts | 29 ++--- .../src/core/context/DragDropProvider.tsx | 102 +++++++++--------- packages/react/src/core/context/context.ts | 2 +- packages/react/src/core/context/hooks.ts | 5 +- packages/react/src/core/context/lifecycle.ts | 23 ---- .../react/src/core/hooks/useDragOperation.ts | 5 +- packages/react/src/core/hooks/useInstance.ts | 13 ++- packages/react/src/hooks/useConstant.ts | 8 +- packages/react/src/sortable/useSortable.ts | 16 +-- 17 files changed, 153 insertions(+), 142 deletions(-) create mode 100644 .changeset/refactor-lifecycle.md delete mode 100644 packages/react/src/core/context/lifecycle.ts diff --git a/.changeset/refactor-lifecycle.md b/.changeset/refactor-lifecycle.md new file mode 100644 index 00000000..ac60539a --- /dev/null +++ b/.changeset/refactor-lifecycle.md @@ -0,0 +1,7 @@ +--- +'@dnd-kit/abstract': patch +'@dnd-kit/dom': patch +'@dnd-kit/react': patch +--- + +Refactor the lifecycle to allow `manager` to be optional and provided later during the lifecycle of `draggable` / `droppable` / `sortable` instances. diff --git a/packages/abstract/src/core/entities/draggable/draggable.ts b/packages/abstract/src/core/entities/draggable/draggable.ts index 7db7b6e1..7524c0ec 100644 --- a/packages/abstract/src/core/entities/draggable/draggable.ts +++ b/packages/abstract/src/core/entities/draggable/draggable.ts @@ -17,7 +17,7 @@ export interface Input extends EntityInput { export class Draggable extends Entity { constructor( {modifiers, type, sensors, ...input}: Input, - public manager: DragDropManager + manager: DragDropManager | undefined ) { super(input, manager); @@ -31,12 +31,15 @@ export class Draggable extends Entity { #modifiers: Modifier[] | undefined; public set modifiers(modifiers: Modifiers | undefined) { + const {manager} = this; + this.#modifiers?.forEach((modifier) => modifier.destroy()); + if (!manager) return; this.#modifiers = modifiers?.map((modifier) => { const {plugin, options} = descriptor(modifier); - return new plugin(this.manager, options); + return new plugin(manager, options); }); } @@ -52,9 +55,7 @@ export class Draggable extends Entity { */ @derived public get isDragSource() { - const {dragOperation} = this.manager; - - return dragOperation.source?.id === this.id; + return this.manager?.dragOperation.source?.id === this.id; } public destroy() { diff --git a/packages/abstract/src/core/entities/droppable/droppable.ts b/packages/abstract/src/core/entities/droppable/droppable.ts index 94e248cf..40aad4b2 100644 --- a/packages/abstract/src/core/entities/droppable/droppable.ts +++ b/packages/abstract/src/core/entities/droppable/droppable.ts @@ -26,7 +26,7 @@ export class Droppable extends Entity { type, ...input }: Input, - public manager: DragDropManager + manager: DragDropManager | undefined ) { super(input, manager); @@ -91,7 +91,7 @@ export class Droppable extends Entity { @derived public get isDropTarget() { - return this.manager.dragOperation.target?.id === this.id; + return this.manager?.dragOperation.target?.id === this.id; } public refreshShape() { diff --git a/packages/abstract/src/core/entities/entity/entity.ts b/packages/abstract/src/core/entities/entity/entity.ts index b0709b26..699abd9b 100644 --- a/packages/abstract/src/core/entities/entity/entity.ts +++ b/packages/abstract/src/core/entities/entity/entity.ts @@ -1,6 +1,6 @@ -import {effects, reactive, type Effect} from '@dnd-kit/state'; +import {reactive, type Effect} from '@dnd-kit/state'; -import type {DragDropManager} from '../../manager/index.ts'; +import {DragDropManager} from '../../manager/index.ts'; import type {Data, UniqueIdentifier} from './types.ts'; interface Options { @@ -36,7 +36,7 @@ export class Entity { * @param input - An object containing the initial properties of the entity. * @param manager - The manager that controls the drag and drop operations. */ - constructor(input: Input, manager: DragDropManager) { + constructor(input: Input, manager: DragDropManager | undefined) { const { effects: getEffects = getDefaultEffects, id, @@ -54,15 +54,15 @@ export class Entity { this.effects = () => [ () => { // Re-run this effect whenever the `id` changes - const {id: _} = this; + const {id: _, manager} = this; if (id === previousId) { return; } - manager.registry.register(this); + manager?.registry.register(this); - return () => manager.registry.unregister(this); + return () => manager?.registry.unregister(this); }, ...getEffects(), ]; @@ -70,7 +70,7 @@ export class Entity { if (options?.register !== false) { queueMicrotask(() => { - manager.registry.register(this); + this.manager?.registry.register(this); }); } } @@ -109,6 +109,6 @@ export class Entity { * @returns void */ public destroy(): void { - this.manager.registry.unregister(this); + this.manager?.registry.unregister(this); } } diff --git a/packages/abstract/src/core/manager/manager.ts b/packages/abstract/src/core/manager/manager.ts index 7f8dddb1..e04859b9 100644 --- a/packages/abstract/src/core/manager/manager.ts +++ b/packages/abstract/src/core/manager/manager.ts @@ -55,6 +55,7 @@ export class DragDropManager< this.plugins = [CollisionNotifier, ...plugins]; this.modifiers = modifiers; this.sensors = sensors; + this.destroy = this.destroy.bind(this); } get plugins(): Plugin[] { diff --git a/packages/dom/src/core/entities/draggable/draggable.ts b/packages/dom/src/core/entities/draggable/draggable.ts index a587e7f2..7b1d9247 100644 --- a/packages/dom/src/core/entities/draggable/draggable.ts +++ b/packages/dom/src/core/entities/draggable/draggable.ts @@ -42,7 +42,7 @@ export class Draggable extends AbstractDraggable { feedback = 'default', ...input }: Input, - public manager: AbstractDragDropManager + manager: AbstractDragDropManager | undefined ) { super( { @@ -50,6 +50,9 @@ export class Draggable extends AbstractDraggable { ...effects(), () => { const {manager} = this; + + if (!manager) return; + const sensors = this.sensors?.map(descriptor) ?? [ ...manager.sensors, ]; diff --git a/packages/dom/src/core/entities/droppable/droppable.ts b/packages/dom/src/core/entities/droppable/droppable.ts index 19bcbe78..42470495 100644 --- a/packages/dom/src/core/entities/droppable/droppable.ts +++ b/packages/dom/src/core/entities/droppable/droppable.ts @@ -26,7 +26,7 @@ export interface Input export class Droppable extends AbstractDroppable { constructor( {element, effects = () => [], ...input}: Input, - public manager: AbstractDragDropManager + manager: AbstractDragDropManager | undefined ) { const {collisionDetector = defaultCollisionDetection} = input; @@ -38,6 +38,8 @@ export class Droppable extends AbstractDroppable { ...effects(), () => { const {element, manager} = this; + if (!manager) return; + const {dragOperation} = manager; if (element && dragOperation.status.initialized) { @@ -92,6 +94,9 @@ export class Droppable extends AbstractDroppable { }, () => { const {manager} = this; + + if (!manager) return; + const {dragOperation} = manager; const {status} = dragOperation; const source = untracked(() => dragOperation.source); @@ -105,9 +110,7 @@ export class Droppable extends AbstractDroppable { } }, () => { - const {manager} = this; - - if (manager.dragOperation.status.initialized) { + if (this.manager?.dragOperation.status.initialized) { return () => { this.shape = undefined; }; @@ -127,7 +130,7 @@ export class Droppable extends AbstractDroppable { * If a droppable target mounts during a drag operation, assume it is visible * so that we can update its shape immediately. */ - if (manager.dragOperation.status.initialized) { + if (this.manager?.dragOperation.status.initialized) { this.visible = true; } } diff --git a/packages/dom/src/sortable/sortable.ts b/packages/dom/src/sortable/sortable.ts index 9c4cb6ff..14509e2b 100644 --- a/packages/dom/src/sortable/sortable.ts +++ b/packages/dom/src/sortable/sortable.ts @@ -112,7 +112,7 @@ export class Sortable { plugins = defaultPlugins, ...input }: SortableInput, - public manager: DragDropManager + manager: DragDropManager | undefined ) { this.droppable = new SortableDroppable(input, manager, this); this.draggable = new SortableDraggable( @@ -120,12 +120,12 @@ export class Sortable { ...input, effects: () => [ () => - this.manager.monitor.addEventListener('dragstart', () => { + this.manager?.monitor.addEventListener('dragstart', () => { this.initialIndex = this.index; this.previousIndex = this.index; }), () => { - const {index, previousIndex} = this; + const {index, previousIndex, manager: _} = this; // Re-run this effect whenever the index changes if (index === previousIndex) { @@ -144,6 +144,13 @@ export class Sortable { this.droppable.disabled = !target; } }, + () => { + const {manager} = this; + + for (const plugin of plugins) { + manager?.registry.register(plugin); + } + }, ...inputEffects(), ], type, @@ -153,10 +160,7 @@ export class Sortable { this ); - for (const plugin of plugins) { - manager.registry.register(plugin); - } - + this.manager = manager; this.index = index; this.previousIndex = index; this.initialIndex = index; @@ -174,11 +178,15 @@ export class Sortable { untracked(() => { const {manager, transition} = this; const {shape} = this.droppable; + + if (!manager) return; + const {idle} = manager.dragOperation.status; if (!shape || !transition || (idle && !transition.idle)) { return; } + scheduler.schedule(() => { const {element} = this.droppable; @@ -220,6 +228,15 @@ export class Sortable { }); } + public get manager(): DragDropManager | undefined { + return this.draggable.manager as any; + } + + public set manager(manager: DragDropManager | undefined) { + this.draggable.manager = manager as any; + this.droppable.manager = manager as any; + } + public set element(element: Element | undefined) { this.draggable.element = element; this.droppable.element = element; @@ -336,7 +353,7 @@ export class Sortable { export class SortableDraggable extends Draggable { constructor( input: DraggableInput, - manager: DragDropManager, + manager: DragDropManager | undefined, public sortable: Sortable ) { super(input, manager); @@ -350,7 +367,7 @@ export class SortableDraggable extends Draggable { export class SortableDroppable extends Droppable { constructor( input: DraggableInput, - manager: DragDropManager, + manager: DragDropManager | undefined, public sortable: Sortable ) { super(input, manager); diff --git a/packages/helpers/src/move.ts b/packages/helpers/src/move.ts index f390e30b..2ac43d1c 100644 --- a/packages/helpers/src/move.ts +++ b/packages/helpers/src/move.ts @@ -65,18 +65,21 @@ function mutate< if (sourceIndex === -1 || targetIndex === -1) { return items; } - const {dragOperation} = source.manager; - - // Reconcile optimistic updates - if ( - !dragOperation.canceled && - 'index' in source && - typeof source.index === 'number' - ) { - const projectedSourceIndex = source.index; - - if (projectedSourceIndex !== sourceIndex) { - return mutation(items, sourceIndex, projectedSourceIndex); + + if (source.manager) { + const {dragOperation} = source.manager; + + // Reconcile optimistic updates + if ( + !dragOperation.canceled && + 'index' in source && + typeof source.index === 'number' + ) { + const projectedSourceIndex = source.index; + + if (projectedSourceIndex !== sourceIndex) { + return mutation(items, sourceIndex, projectedSourceIndex); + } } } @@ -116,6 +119,8 @@ function mutate< } } + if (!source.manager) return items; + const {dragOperation} = source.manager; const position = dragOperation.position.current; diff --git a/packages/react/src/core/context/DragDropProvider.tsx b/packages/react/src/core/context/DragDropProvider.tsx index 9c4a0cf9..8e41810e 100644 --- a/packages/react/src/core/context/DragDropProvider.tsx +++ b/packages/react/src/core/context/DragDropProvider.tsx @@ -1,12 +1,16 @@ -import {useEffect, type PropsWithChildren} from 'react'; +import { + startTransition, + useEffect, + useState, + type PropsWithChildren, +} from 'react'; import type {DragDropEvents} from '@dnd-kit/abstract'; import {DragDropManager, defaultPreset} from '@dnd-kit/dom'; import type {DragDropManagerInput, Draggable, Droppable} from '@dnd-kit/dom'; -import {useConstant, useLatest, useOnValueChange} from '@dnd-kit/react/hooks'; +import {useLatest, useOnValueChange} from '@dnd-kit/react/hooks'; import {DragDropContext} from './context.ts'; import {useRenderer} from './renderer.ts'; -import {Lifecycle} from './lifecycle.ts'; type Events = DragDropEvents; @@ -31,12 +35,9 @@ export default function DragDropProvider({ ...input }: Props) { const {renderer, trackRendering} = useRenderer(); - const createManager = () => { - const instance = input.manager ?? new DragDropManager(input); - instance.renderer = renderer; - return instance; - }; - const manager = useConstant(createManager); + const [manager, setManager] = useState( + input.manager ?? null + ); const {plugins, modifiers, sensors} = input; const handleBeforeDragStart = useLatest(onBeforeDragStart); const handleDragStart = useLatest(onDragStart); @@ -46,61 +47,64 @@ export default function DragDropProvider({ const handleCollision = useLatest(onCollision); useEffect(() => { - const listeners = [ - manager.monitor.addEventListener('beforedragstart', (event, manager) => { - const callback = handleBeforeDragStart.current; + const manager = input.manager ?? new DragDropManager(input); + manager.renderer = renderer; + + manager.monitor.addEventListener('beforedragstart', (event, manager) => { + const callback = handleBeforeDragStart.current; + + if (callback) { + trackRendering(() => callback(event, manager)); + } + }); + manager.monitor.addEventListener('dragstart', (event, manager) => + handleDragStart.current?.(event, manager) + ); + manager.monitor.addEventListener('dragover', (event, manager) => { + const callback = handleDragOver.current; - if (callback) { - trackRendering(() => callback(event, manager)); - } - }), - manager.monitor.addEventListener('dragstart', (event, manager) => - handleDragStart.current?.(event, manager) - ), - manager.monitor.addEventListener('dragover', (event, manager) => { - const callback = handleDragOver.current; + if (callback) { + trackRendering(() => callback(event, manager)); + } + }); + manager.monitor.addEventListener('dragmove', (event, manager) => { + const callback = handleDragMove.current; - if (callback) { - trackRendering(() => callback(event, manager)); - } - }), - manager.monitor.addEventListener('dragmove', (event, manager) => { - const callback = handleDragMove.current; + if (callback) { + trackRendering(() => callback(event, manager)); + } + }); + manager.monitor.addEventListener('dragend', (event, manager) => { + const callback = handleDragEnd.current; - if (callback) { - trackRendering(() => callback(event, manager)); - } - }), - manager.monitor.addEventListener('dragend', (event, manager) => { - const callback = handleDragEnd.current; + if (callback) { + trackRendering(() => callback(event, manager)); + } + }); + manager.monitor.addEventListener('collision', (event, manager) => + handleCollision.current?.(event, manager) + ); - if (callback) { - trackRendering(() => callback(event, manager)); - } - }), - manager.monitor.addEventListener('collision', (event, manager) => - handleCollision.current?.(event, manager) - ), - ]; + startTransition(() => setManager(manager)); - return () => { - listeners.forEach((dispose) => dispose()); - }; - }, []); + return manager.destroy; + }, [renderer]); useOnValueChange( plugins, - () => (manager.plugins = plugins ?? defaultPreset.plugins) + () => manager && (manager.plugins = plugins ?? defaultPreset.plugins) ); useOnValueChange( sensors, - () => (manager.sensors = sensors ?? defaultPreset.sensors) + () => manager && (manager.sensors = sensors ?? defaultPreset.sensors) + ); + useOnValueChange( + modifiers, + () => manager && (manager.modifiers = modifiers ?? []) ); - useOnValueChange(modifiers, () => (manager.modifiers = modifiers ?? [])); return ( - {children} ); diff --git a/packages/react/src/core/context/context.ts b/packages/react/src/core/context/context.ts index 2bf9276f..1ec7ac49 100644 --- a/packages/react/src/core/context/context.ts +++ b/packages/react/src/core/context/context.ts @@ -3,6 +3,6 @@ import {createContext} from 'react'; import {DragDropManager} from '@dnd-kit/dom'; -export const DragDropContext = createContext( +export const DragDropContext = createContext( new DragDropManager() ); diff --git a/packages/react/src/core/context/hooks.ts b/packages/react/src/core/context/hooks.ts index ec3b3aec..c1b5fafe 100644 --- a/packages/react/src/core/context/hooks.ts +++ b/packages/react/src/core/context/hooks.ts @@ -9,10 +9,9 @@ export function useDragDropManager() { export function useDragOperation() { const manager = useDragDropManager(); - const {dragOperation} = manager; - const source = useComputed(() => dragOperation.source); - const target = useComputed(() => dragOperation.target); + const source = useComputed(() => manager?.dragOperation.source); + const target = useComputed(() => manager?.dragOperation.target); return { get source() { diff --git a/packages/react/src/core/context/lifecycle.ts b/packages/react/src/core/context/lifecycle.ts deleted file mode 100644 index ac89a000..00000000 --- a/packages/react/src/core/context/lifecycle.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {Component} from 'react'; -import type {CleanupFunction} from '@dnd-kit/state'; -import {DragDropManager} from '@dnd-kit/dom'; -import {timeout} from '@dnd-kit/dom/utilities'; - -export class Lifecycle extends Component<{manager: DragDropManager}> { - private initialized = false; - private clearTimeout: CleanupFunction | undefined; - - componentDidMount() { - this.clearTimeout?.(); - this.clearTimeout = timeout(() => (this.initialized = true), 25); - } - - componentWillUnmount() { - if (!this.initialized) return; - this.props.manager.destroy(); - } - - render() { - return null; - } -} diff --git a/packages/react/src/core/hooks/useDragOperation.ts b/packages/react/src/core/hooks/useDragOperation.ts index 04037e66..902c3b36 100644 --- a/packages/react/src/core/hooks/useDragOperation.ts +++ b/packages/react/src/core/hooks/useDragOperation.ts @@ -4,10 +4,9 @@ import {useDragDropManager} from './useDragDropManager.ts'; export function useDragOperation() { const manager = useDragDropManager(); - const {dragOperation} = manager; - const source = useComputed(() => dragOperation.source); - const target = useComputed(() => dragOperation.target); + const source = useComputed(() => manager?.dragOperation.source); + const target = useComputed(() => manager?.dragOperation.target); return { get source() { diff --git a/packages/react/src/core/hooks/useInstance.ts b/packages/react/src/core/hooks/useInstance.ts index e0ef6e07..223dd640 100644 --- a/packages/react/src/core/hooks/useInstance.ts +++ b/packages/react/src/core/hooks/useInstance.ts @@ -1,22 +1,21 @@ -import {useEffect} from 'react'; +import {useEffect, useState} from 'react'; import {Entity} from '@dnd-kit/abstract'; import type {DragDropManager} from '@dnd-kit/dom'; -import {useConstant} from '@dnd-kit/react/hooks'; import {useDragDropManager} from './useDragDropManager.ts'; export function useInstance( - initializer: (manager: DragDropManager) => T + initializer: (manager: DragDropManager | undefined) => T ): T { - const manager = useDragDropManager(); - const instance = useConstant(() => initializer(manager)); + const manager = useDragDropManager() ?? undefined; + const [instance] = useState(() => initializer(manager)); useEffect(() => { instance.manager = manager as any; // Register returns an unregister callback - return manager.registry.register(instance); - }, [manager]); + return manager?.registry.register(instance); + }, [instance, manager]); return instance; } diff --git a/packages/react/src/hooks/useConstant.ts b/packages/react/src/hooks/useConstant.ts index 929dab04..98814419 100644 --- a/packages/react/src/hooks/useConstant.ts +++ b/packages/react/src/hooks/useConstant.ts @@ -1,17 +1,11 @@ import {useRef} from 'react'; -export function useConstant(initializer: () => T, dependency?: any) { +export function useConstant(initializer: () => T) { const ref = useRef(); - const previousDependency = useRef(dependency); if (!ref.current) { ref.current = initializer(); } - if (previousDependency.current !== dependency) { - previousDependency.current = dependency; - ref.current = initializer(); - } - return ref.current; } diff --git a/packages/react/src/sortable/useSortable.ts b/packages/react/src/sortable/useSortable.ts index 347c2ba9..b8ebfd7c 100644 --- a/packages/react/src/sortable/useSortable.ts +++ b/packages/react/src/sortable/useSortable.ts @@ -1,4 +1,4 @@ -import {useCallback, useLayoutEffect} from 'react'; +import {useCallback, useEffect} from 'react'; import {deepEqual} from '@dnd-kit/state'; import {type Data} from '@dnd-kit/abstract'; import {Sortable, defaultSortableTransition} from '@dnd-kit/dom/sortable'; @@ -35,12 +35,10 @@ export function useSortable(input: UseSortableInput) { transition = defaultSortableTransition, type, } = input; - const manager = useDragDropManager(); const handle = currentValue(input.handle); const element = currentValue(input.element); const target = currentValue(input.target); - const sortable = useConstant(() => { return new Sortable( { @@ -54,11 +52,15 @@ export function useSortable(input: UseSortableInput) { register: false, }, }, - manager + manager ?? undefined ); - }, manager); + }); + + useEffect(() => { + sortable.manager = manager ?? undefined; + + if (!manager) return; - useLayoutEffect(() => { manager.registry.register(sortable.draggable); manager.registry.register(sortable.droppable); @@ -85,7 +87,7 @@ export function useSortable(input: UseSortableInput) { useOnValueChange( index, () => { - if (manager.dragOperation.status.idle && transition) { + if (manager?.dragOperation.status.idle && transition) { sortable.refreshShape(); } },