SoFunction
Updated on 2025-04-07

Detailed explanation of how React DnD handles drag and drop

text

React DnD is a React drag and drop library focusing on data changes. In simple terms, what you drag and drop is not the page view, but the data. React DnD does not provide a cool drag experience, but instead helps us manage data changes in dragging and then renders them based on this data. We may need to write additional view layers to achieve the desired effect, but this drag and drop management method is very universal and can be used in any scenario. It may not feel that it is so convenient for the first time, but if the scene is more complicated or requires high customization, React DnD must be the first choice.

React DnD instructions can be found inOfficial Documentation. This article analyzes the source code of React DnD and has a deeper understanding of this library. The following code is derived from react-dnd 14.0.4.

Code structure

React-DnD is a single repository, but multiple packages are employed. This method also represents the three-layer structure of React DnD.

___________     ___________     _______________
|           |   |           |   |               | 
|           |   |           |   | backend-html  |
| react-dnd |   |  dnd-core |   |               |  
|           |   |           |   | backend-touch |
|___________|   |___________|   |_______________|

react-dnd is an implementation of the React version of Drag and Drop. It defines advanced components such as DragSource, DropTarget, DragDropContext, and hooks such as useDrag, useDrop, etc. We can simply understand that this is an access layer.

dnd-core is the core of the entire drag and drop library. It implements a framework-independent drag and drop manager, defines the drag and drop interaction. According to the rules defined in dnd-core, we can implement a vue-dnd based on it. Use redux for state management in dnd-core.

backend is where React DnD abstracts the concept of backend, and here is where DOM events are converted to redux action. If it is an H5 application, backend-html, and if it is a mobile application, use backend-touch. User customization is also supported.

DndProvider

If you want to use React DnD, you need to add a DndProvider to the outer element.

import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
<DndProvider backend={HTML5Backend}>
    <TutorialApp />
</DndProvider>

The essence of DndProvider is a container (component) that creates a context to control the behavior of dragging and data sharing. The entry parameter of DndProvider is a Backend. What is Backend? React DnD separates the code related to the DOM event and converts the drag and drop events into redux action inside React DnD. Since dragging occurs ondrag when H5, it is simulated by touch when it occurs on mobile devices. React DnD extracts this part separately to facilitate subsequent expansion. This part is called Backend. It is an implementation of DnD at the Dom layer.

The following is the core code of DndProvider. A manager is generated by entering parameters, which is used to control drag and drop behavior. Put this manager in the Provider, and all children can access this manager.

export const DndProvider: FC<DndProviderProps<unknown, unknown>> = memo(
    function DndProvider({ children, ...props }) {
        const [manager, isGlobalInstance] = getDndContextValue(props)
    ...
        return < value={manager}>{children}</>
    },
)

DragDropManager

DndProvider places DndProvider in context. This manager is very critical. Subsequent drags depend on the manager. The following is its creation process.

export function createDragDropManager(
    backendFactory: BackendFactory,
    globalContext: unknown = undefined,
    backendOptions: unknown = {},
    debugMode = false,
): DragDropManager {
    const store = makeStoreInstance(debugMode)
    const monitor = new DragDropMonitorImpl(store, new HandlerRegistryImpl(store))
    const manager = new DragDropManagerImpl(store, monitor)
    const backend = backendFactory(manager, globalContext, backendOptions)
    (backend)
    return manager
}

First, let’s look at the creation process of the store. The creation of the store in the manager uses the createStore method of redux, which is used to store all states in the application. Its first parameter reducer receives two parameters, namely the current state tree and the action to be processed, and returns the new state tree.

function makeStoreInstance(): Store<State> {
    return createStore(reduce)
}

The store in the manager manages the following states, each state has a corresponding method to update.

export interface State {
    dirtyHandlerIds: DirtyHandlerIdsState
    dragOffset: DragOffsetState
    refCount: RefCountState
    dragOperation: DragOperationState
    stateId: StateIdState
}

The standard method of redux to update data is the dispatch action method. The following is the dragOffset update method, determine the type of the current action, obtain the required parameters from the payload, and then return to the new state.

export function reduce(
    state: State = initialState,
    action: Action&lt;{
        sourceClientOffset: XYCoord
        clientOffset: XYCoord
    }&gt;,
): State {
    const { payload } = action
    switch () {
        case INIT_COORDS:
        case BEGIN_DRAG:
            return {
                initialSourceClientOffset: ,
                initialClientOffset: ,
                clientOffset: ,
            }
        case HOVER:
      ...
        case END_DRAG:
        case DROP:
            return initialState
        default:
            return state
    }
}

Next, let’s look at the monitor. It is known that the store represents the data during the dragging process. Then we can calculate some current states based on these data, such as whether an object can be dragged, whether an object is hanging in the air, etc. Monitor provides some methods to access this data. Not only that, the biggest function of monitor is to monitor this data. We can add some listeners to monitor so that we can respond in time after the data changes.

Some methods in monitor are listed below.

export interface DragDropMonitor {
    subscribeToStateChange(
        listener: Listener,
        options?: {
            handlerIds: Identifier[] | undefined
        },
    ): Unsubscribe
    subscribeToOffsetChange(listener: Listener): Unsubscribe
    canDragSource(sourceId: Identifier | undefined): boolean
    canDropOnTarget(targetId: Identifier | undefined): boolean
    isDragging(): boolean
    isDraggingSource(sourceId: Identifier | undefined): boolean
    getItemType(): Identifier | null
    getItem(): any
    getSourceId(): Identifier | null
    getTargetIds(): Identifier[]
    getDropResult(): any
    didDrop(): boolean
  ...
}

subscribeToStateChange is a method to add a listening function. The principle is to use the subscribe method of redux.

public subscribeToStateChange(
        listener: Listener,
        options: { handlerIds: string[] | undefined } = { handlerIds: undefined },
    ): Unsubscribe {
  ...
  return (handleChange)
}

It should be noted that DragDropMonitor is a global monitor, and its listening range is all draggable elements under DndProvider, that is, there will be multiple objects in the monitor, and these dragged objects have global unique ID identifiers (ids that increase from 0). This is also the reason why some methods in the monitor need to pass an Identifier. Another point is that it is best not to have multiple DndProviders, unless you are sure that dragging elements under different DndProviders will not interact.

We passed in DndProvider a parameter backend. In fact, it is a factory method. After execution, the real backend will be generated.

Manager is relatively simple. It contains the monitor, store, and backend generated previously. It also adds a listener to the store during initialization. It listens to the refCount method in state, which represents the object currently marked as draggable. If refCount is greater than 0, initializes the backend, otherwise, destroys the backend.

export class DragDropManagerImpl implements DragDropManager {
    private store: Store<State>
    private monitor: DragDropMonitor
    private backend: Backend | undefined
    private isSetUp = false
    public constructor(store: Store<State>, monitor: DragDropMonitor) {
         = store
         = monitor
        ()
    }
  ...
    private handleRefCountChange = (): void => {
        const shouldSetUp = ().refCount > 0
        if () {
            if (shouldSetUp && !) {
                ()
                 = true
            } else if (!shouldSetUp && ) {
                ()
                 = false
            }
        }
    }
}

The manager is created, which means that at this time we have a store to manage the drag data, a monitor to monitor data and control behavior, can register through manager, and can convert Dom events into action through backend. It will be used next useDrag Create a real draggable object。

useDrag

If an element wants to be dragged, the Hooks is written as follows, using useDrag to implement it. The entry parameter and return value of useDrag can be referencedOfficial Documentation, I will not go into details here.

import { DragPreviewImage, useDrag } from 'react-dnd';
export const Knight: FC = () => {
    const [{ isDragging }, drag, preview] = useDrag(
        () => ({
            type: ,
            collect: (monitor) => ({
                isDragging: !!()
            })
        }),
        []
    );
    return (
        <>
            <DragPreviewImage connect={preview} src={knightImage} />
            <div
                ref={drag}
            >
                ♘
            </div>
        </>
    );
};

When using useDrag, we configure the entry parameter, which is a function. The return value of this function is the configuration parameter. UseOptionalFactory uses useMemo to wrap this method in a layer to avoid repeated calls.

export function useDrag&lt;DragObject, DropResult, CollectedProps&gt;(
    specArg: FactoryOrInstance&lt;
        DragSourceHookSpec&lt;DragObject, DropResult, CollectedProps&gt;
    &gt;,
    deps?: unknown[],
): [CollectedProps, ConnectDragSource, ConnectDragPreview] {
  // Get configuration parameters    const spec = useOptionalFactory(specArg, deps)
  // Get the wrapper object of the monitor in the manager (DragSourceMonitor)    const monitor = useDragSourceMonitor&lt;DragObject, DropResult&gt;()
    // Connect to dom and redux    const connector = useDragSourceConnector(, )
    // Generate a unique id and encapsulate the DragSource object    useRegisteredDragSource(spec, monitor, connector)
    return [
        useCollectedProps(, monitor, connector),
        useConnectDragSource(connector),
        useConnectDragPreview(connector),
    ]
}

The monitor type in the manager was DragDropMonitor. You can tell by the name that the method in this monitor combines the behavior of Drag and Drop. Currently, it only uses Drag, so the monitor is wrapped to block Drop's behavior. Make its type DragSourceMonitor. This is what useDragSourceMonitor does,

export function useDragSourceMonitor<O, R>(): DragSourceMonitor<O, R> {
    const manager = useDragDropManager()
    return useMemo(() => new DragSourceMonitorImpl(manager), [manager])
}

Above, we have Backend that controls the behavior of the Dom level, and Store and Monitor control changes in the data layer. How to let Monitor know which node to listen to now, and you also need to connect the two to truly keep the Dom level and the data layer consistent. React DnD uses connector to connect the two.

The useDragSourceConnector method will new an instance of SourceConnector, which will accept backend as an entry parameter. SourceConnector implements the Connector interface. There are not many member variables in the Connector, the most important thing is the hooks object, which is used to process the logic of ref.

export interface Connector {
    // Get the Dom pointed to by ref    hooks: any
    // Get dragSource    connectTarget: any
    // dragSource unique Id    receiveHandlerId(handlerId: Identifier | null): void
    // Reconnect dragSource and dom    reconnect(): void
}

In the example, we give the ref attribute a return value of useDrag. This return value is actually the dragSource method in hooks.

export function useConnectDragSource(connector: SourceConnector) {
    return useMemo(() =&gt; (), [connector])
}

From the dragSource method, it can be seen that the connector maintains this Dom node on the dragSourceNode property.

export class SourceConnector implements Connector {
    // wrapConnectorHooks determines whether the ref node is a legal ReactElement. If so, execute the callback method.    public hooks = wrapConnectorHooks({
        dragSource: (
            node: Element | ReactElement | Ref&lt;any&gt;,
            options?: DragSourceOptions,
        ) =&gt; {
            // dragSourceRef and dragSourceNode assignment null            ()
             = options || null
            if (isRef(node)) {
                 = node as RefObject&lt;any&gt;
            } else {
                 = node
            }
            ()
        },
        ...
    })
    ...
}

After obtaining the node, call (). In this method, backend calls the connectDragSource method to add event listening to the node, and backend will be analyzed in the future.

private reconnectDragSource() {
    const dragSource = 
    ...
    if (didChange) {
        ...
         = (
            ,
            dragSource,
            ,
        )
    }
}

Now we need to abstract Dom, generate a unique ID, encapsulate it as DragSource and register it on the monitor.

export function useRegisteredDragSource&lt;O, R, P&gt;(
    spec: DragSourceHookSpec&lt;O, R, P&gt;,
    monitor: DragSourceMonitor&lt;O, R&gt;,
    connector: SourceConnector,
): void {
    const manager = useDragDropManager()
    // Generate DragSource    const handler = useDragSource(spec, monitor, connector)
    const itemType = useDragType(spec)
    // useLayoutEffect
    useIsomorphicLayoutEffect(
        function registerDragSource() {
            if (itemType != null) {
                // Register DragSource to monitor                const [handlerId, unregister] = registerSource(
                    itemType,
                    handler,
                    manager,
                )
                // Update the unique ID and trigger the reconnect logic                (handlerId)
                (handlerId)
                return unregister
            }
        },
        [manager, monitor, connector, handler, itemType],
    )
}

DragSource implements the following methods. When we use useDarg, we can configure functions of the same name. These configured methods will be called by the following methods.

export interface DragSource {
    beginDrag(monitor: DragDropMonitor, targetId: Identifier): void
    endDrag(monitor: DragDropMonitor, targetId: Identifier): void
    canDrag(monitor: DragDropMonitor, targetId: Identifier): boolean
    isDragging(monitor: DragDropMonitor, targetId: Identifier): boolean
}

To summarize what useDarg does, the first thing is to support some configuration parameters, which are the most basic, and then obtain the managre in Provider, wrap some of the objects, block some methods, and add some parameters. The most important thing is to create a connector. After the interface is loaded, the connector obtains an instance of the Dom node through ref, and adds drag attributes and drag events to the node. At the same time, encapsulate the DragSource object according to the configuration parameters and connector and register it into the monitor.

The processes of useDrop and useDrag are similar, you can watch them yourself.

HTML5Backend

The parameter HTML5Backend injected for DndProvider is actually an engineering method. In addition to configuring backend in DndProvider, we can also configure some parameters of backend. Of course, different implementations of backend and parameters are also different. DragDropManager initializes the true backend based on these parameters.

export const HTML5Backend: BackendFactory = function createBackend(
    manager: DragDropManager,
    context?: HTML5BackendContext,
    options?: HTML5BackendOptions,
): HTML5BackendImpl {
    return new HTML5BackendImpl(manager, context, options)
}

The following is the method that Backend needs to be implemented.

export interface Backend {
    setup(): void
    teardown(): void
    connectDragSource(sourceId: any, node?: any, options?: any): Unsubscribe
    connectDragPreview(sourceId: any, node?: any, options?: any): Unsubscribe
    connectDropTarget(targetId: any, node?: any, options?: any): Unsubscribe
    profile(): Record<string, number>
}

setup is the initialization method of backend, teardown is the backend destroy method. As mentioned above, setup and teardown are executed in handleRefCountChange. React DnD will execute the setup method when we are the first to use useDrag or useDrop, and execute the teardown method when it detects that there is nowhere to use the drag function.

The following method is executed in the setup method of HTML5BackendImpl. Target refers to window by default. All drag events are listened here. This is a typical event delegate method. The unified binding of the callback function of the drag event to the window can not only improve performance, but also greatly reduce the difficulty of event destruction.

private addEventListeners(target: Node) {
    if (!) {
        return
    }
    (
        'dragstart',
         as EventListener,
    )
    ('dragstart', , true)
    ('dragend', , true)
    (
        'dragenter',
         as EventListener,
    )
    (
        'dragenter',
         as EventListener,
        true,
    )
    (
        'dragleave',
         as EventListener,
        true,
    )
    ('dragover',  as EventListener)
    ('dragover', , true)
    ('drop',  as EventListener)
    (
        'drop',
         as EventListener,
        true,
    )
}

HTML5Backend The drag-drop monitoring function is to obtain the object of the drag event and get the corresponding parameters. HTML5Backend gets an instance of DragDropActions through the Manager and executes the method. DragDropActions is essentially encapsulating it into an action based on parameters, and finally distributing the action through redux dispatch to change the data in the store.

export interface DragDropActions {
    beginDrag(
        sourceIds?: Identifier[],
        options?: any,
    ): Action<BeginDragPayload> | undefined
    publishDragSource(): SentinelAction | undefined
    hover(targetIds: Identifier[], options?: any): Action<HoverPayload>
    drop(options?: any): void
    endDrag(): SentinelAction
}

Let's take a look at the connectDragSource method. This method is used to convert a Node node into a draggable node and add a listening event.

HTML5Backend is implemented using the HTML5 drag-and-drop API. First: To set an element to drag-and-drop, set the draggable property to true. Then listen for the ondragstart event, which is triggered when the user starts dragging the element. As for selectstart, don't worry, it is used to deal with some special IE situations.

public connectDragSource(
    sourceId: string,
    node: Element,
    options: any,
): Unsubscribe {
    ...
    // Set draggable property    ('draggable', 'true')
    // Add dragstart monitor    ('dragstart', handleDragStart)
        // Add selectstart listening    ('selectstart', handleSelectStart)
    ...
}

The dragstart event bound on Node is very simple, it is to update the sourceId. The logic responsible is bound to the window.

public handleDragStart(e: DragEvent, sourceId: string): void {
    if () {
        return
    }
    if (!) {
         = []
    }
    (sourceId)
}

In summary, when HTML5Backend is initialized, it binds the listening function of the drag event on the window, processes the coordinate data and status data in the drag, and converts it into action and handed over to the upper store for processing. Complete the transformation from Dom event to data. Listeners bound on elements are only responsible for updating sourceId.

TouchBackend

Finally, let’s take a look at TouchBackend. Compared with HTML5Backend, TouchBackend has a wider usage scenario because it does not rely on the H5 API and has good compatibility. It can be used both on the browser and on the mobile side.

TouchBackend uses simple events to simulate drag and drop behavior. For example, on the browser side, mousedown, mousemove, mouseup is used. Mobile uses touchstart, touchmove, and touchend.

const eventNames: Record&lt;ListenerType, EventName&gt; = {
    []: {
        start: 'mousedown',
        move: 'mousemove',
        end: 'mouseup',
        contextmenu: 'contextmenu',
    },
    []: {
        start: 'touchstart',
        move: 'touchmove',
        end: 'touchend',
    },
    []: {
        keydown: 'keydown',
    },
}

Summarize

This article analyzes how React-DnD handles drag and drop.

First of all, React-DnD uses a layered design method. react-dnd is the access layer, which prepares two methods: high-order components and Hooks. dnd-core is the core, which defines the drag and drop interface, management method, and data flow direction. Where the DOM event is converted to redux action in backend, this layer is used to block differences between devices.

dnd-core uses redux to manage data, which is modified through dispatch action, monitored by data, and connect to dom and store using connector. The final drag implementation relies on backend, adds a listening event to the node, and then converts the event into an action.

Overall, the core idea of ​​React-DnD is to convert events into data, and the design refers to the single data stream of redux (after all, written by an author). In this way, when we handle dragging, we can pay attention to data changes, without having to worry about maintaining some intermediate states in dragging, let alone adding and removing events by ourselves. This is a very good design.

The above is the detailed explanation of how React DnD handles drag and drop. For more information about React DnD handled drag and drop, please follow my other related articles!