基于鼠标监听实现元素拖拽的React Hook

1,627 阅读1分钟

实现一个 [X, Y] = useDrag(ref, {X: 10, Y: 10}) 的元素拖拽逻辑

// useDrag.ts 文件

import { useEffect, useCallback, useReducer } from 'react';

interface DragState {
    X: number;
    Y: number;
    mouseX: number;
    mouseY: number;
    moving?: boolean;
}

type DispatchType = 'start' | 'move' | 'stop';

interface DispatchAction {
    type: DispatchType;
    X?: number;
    Y?: number;
    mouseX?: number;
    mouseY?: number;
}

function reducer(state: DragState, action: DispatchAction) {
    const { type, X = 0, Y = 0, mouseX = 0, mouseY = 0 } = action;

    switch (type) {
        case 'start':
            return {
                X,
                Y,
                mouseX,
                mouseY,
                moving: true
            };
        case 'move':
            if (state.moving) {
                return {
                    ...state,
                    X: state.X + mouseX - state.mouseX,
                    Y: state.Y + mouseY - state.mouseY,
                    mouseX,
                    mouseY
                };
            }

            return state;
        case 'stop':
            return {
                ...state,
                moving: false
            };
        default:
            return {
                X: 0,
                Y: 0,
                mouseX: 0,
                mouseY: 0
            };
    }
}

function useDrag(ref, initState) {
    const [state, dispatch] = useReducer(reducer, initState);

    const handleStart = useCallback(
        e => {
            e.preventDefault();

            dispatch({
                type: 'start',
                X: e.currentTarget.offsetLeft,
                Y: e.currentTarget.offsetTop,
                mouseX: e.pageX,
                mouseY: e.pageY
            });
        },
        [ref]
    );

    const handleMove = useCallback(
        e => {
            e.preventDefault();

            dispatch({
                type: 'move',
                mouseX: e.pageX,
                mouseY: e.pageY
            });
        },
        [ref]
    );

    const handleStop = useCallback(
        e => {
            e.preventDefault();

            dispatch({ type: 'stop' });
        },
        [ref]
    );

    useEffect(() => {
        if (!ref) {
            return;
        }

        ref.current.addEventListener('mousedown', handleStart);
        document.addEventListener('mousemove', handleMove);
        document.addEventListener('mouseup', handleStop);

        return () => {
            ref.current.removeEventListener('mousedown', handleStart);
            document.removeEventListener('mousemove', handleMove);
            document.removeEventListener('mouseup', handleStop);
        };
    }, [ref]);

    return [state.X, state.Y];
}

export default useDrag;

// Example.tsx 文件

import React, { FC, useRef, useEffect } from 'react';
import useDrag from './useDrag';

const DragTest: FC = () => {
    const dom = useRef<any>(null);
    const [X, Y] = useDrag(dom, { X: 10, Y: 10 });

    return (
        <div style={{ height: '800px', position: 'relative' }}>
            <div
                style={{
                    position: 'absolute',
                    backgroundColor: 'red',
                    left: X,
                    top: Y,
                    cursor: 'pointer',
                    width: '100px',
                    height: '100px'
                }}
                ref={dom}>
                   X: {X}, Y: {Y}
            </div>
        </div>
    );
};

export default DragTest;