Talking about the use of React-Dnd

Foreword (reason for learning)

Recently, I encountered a new requirement at work: see the following for the specific and general effect.gif

At that time, I saw this requirement and was reminded by my brother to know about React-Dnd. I thought of the effect of a draggable table in Table – Ant Design. I copied it to the project and found that it could not meet our needs, so I searched for some information. Understand the principle of React-Dnd

React-Dnd

React DnD is a React drag-and-drop library that focuses on data changes. It encapsulates the HTML drag-and-drop API. Generally speaking, what you drag and drop changes is not the page view, but the data. React DnD does not provide a cool drag experience, but helps us manage the data changes during drag and drop, and then we render according to these data.

In this way, you can focus on data changes when handling drag and drop, without bothering to maintain some intermediate states in drag and drop, let alone adding and removing events yourself.

First know how to install

tnpm(npm) install react-dnd -S // react-dnd package, its core package
tnpm(npm) install react-dnd-html5-backend -S // The library required for the underlying implementation of drag and drop 

In addition to react-dnd, the react-dnd-html5-backend package is required. Its existence will allow the underlying HTML5 drag and drop AP of React DnD, so that the HTML drag and drop interface enables the application to use the drag and drop function in the browser.

Introduction

//introduction
import {DndProvider, useDrag, useDrop} from 'react-dnd'
import {HTML5Backend} from 'react-dnd-html5-backend'

Three core points

By using the React DnD library, I think the most useful part contains a component and two Hook APIs, which are:

  • DndProvider component
  • useDrag function
  • useDrop function

React-DnD implementation

DndProvider injection

If you want to use React DnD, you first need to add a DndProvider to the outer element, and you must first declare the dragged object

Compare the official explanation:

The DndProvider component provides React-DnD functionality to your application. It must be injected into the backend via the backend parameter, but it can also be injected into the window object.

  • backend: Required, a React DnD backend, the way to realize DnD, there are currently three official documents, namely: react-dnd-html5-backend, react-dnd-touch-backend, react -dnd-test-backend, but react-dnd-html5-backend is commonly used, or you can write backend yourself. .
  • context: Optional, the user configures the context of the backend, which depends on the implementation of the backend.
  • options: Optional, configure the backend object, you can pass in the backend when customizing.

More popular explanation:

DndProvider

The essence is a context container (component) created by React.createContext, which is used to control the behavior of dragging and sharing of data. The input parameter of DndProvider is a Backend.

What is a Backend?

React DnD abstracts the concept of backend. We can use HTML5 to drag and drop the backend, or customize the backend implementation of touch and mouse event simulation. The backend is mainly used to smooth out browser differences, handle DOM events, and at the same time Converted to redux action inside React DnD.

//introduction
import {DndProvider, useDrag, useDrop} from 'react-dnd'
import {HTML5Backend} from 'react-dnd-html5-backend'
//DndProvider injection
export const DndCheckBoxGroup = (props) => {
  const options = props. options
  const setOptions = props. setOptions
  const value = props.value
  const onChange = props.onChange
  return <OptionsContextProvider options={options} setOptions={setOptions} value={value} onChange={onChange}>
    <DndProvider backend={HTML5Backend}>
      //Here will drag and drop related content
      <Dnd Checkbox/>
    </DndProvider>
  </OptionsContextProvider>
}

DndCheckbox component

Write a component to render the list, it can be seen that we want to apply the drag effect to the DraggableCheckbox component

const DndCheckbox = () => {
    const {options, value, onChange} = useContext(OptionsContext);
    return <Checkbox. Group value={value} onChange={onChange}>
        {
            options. map((item, index) => {
                return <DraggableCheckbox key={item.value} item={item} index={index}/>
            })
        }
    </Checkbox.Group>
}

useDrag declares the drag source

What an element needs to know to be dragged is where to drag it from. This library provides the useDrag hook API, which allows a DOM element to achieve a dragging effect.

Code format

import { useDrag } from 'react-dnd'
    /**
     * Returned parameters
     * collected: an object containing the properties collected from the collect function, if collect does not define a function, an empty object is returned
     * drag: the connector function of the dragger, must be attached to the draggable part of the DOM
     * dragPreview: A connector function for drag preview, which can be attached to the preview part of the DOM
     */
const [collected, drag, dragPreview] = useDrag(() => ( () => ({
        // Only the drop and this value can be placed
            type,
        // describe the data to be dragged
            item,
         // monitor function
            collect: (monitor, props) => ({
                isDragging: monitor. isDragging()
            })
        }), [deps])

Join

  • spec specification object or function to create a specification object, the key contents include:
    • item: Required. A plain JavaScript object describing the data to be dragged. This is the only information about the drag source that is available to the drop target
    • type: Required, and must be a string, ES6 symbol. The accept of useDrop is the same type, and the target will respond to this item
    • isDragging(monitor): Optional. By default, only the drag source that initiated the drag operation is considered a drag
    • options: optional, a simple object
    • collect: optional, collection function, it receives two parameters, monitor and props. (useDrag also has)

In addition, there are some related content such as begin(monitor) at the beginning of dragging, end(item, monitor) at the end, whether dragging canDrag(monitor) is allowed, etc., which can be learned in more learning links.

  • deps

Dependency array for memory. This is similar to the built-in useMemo hook. The default is an empty array of function specifications, and an array of specifications containing object specifications.

Return value

  • arguments[0]: An object containing the attributes collected from the collect function. If collect does not define a function, it returns an empty object.
  • arguments[1]: DragSource Ref, the connector function of the drag source. This has to be attached to the draggable part of the DOM.
  • arguments[2]: DragPreview Ref, connector function for drag preview. This can be appended to the preview section of the DOM.

useDrop declare drop source

In order to place the content at the target location, the useDrop Hooks function is provided,

useDrop connects the drop target element with the DnD system. By passing the standard drag object as an input parameter to useDrop, you can define the data items accepted by the drop target, which collects to use, and so on. The function returns an array containing a ref and collected props to be attached to the Drop Target node.

code format

const [collectedProps, drop] = useDrop(() => (
  //spec
           {
   // This drop target will only react to items spawned by drag sources of the specified type
            accept,
    // called when a compatible item is placed on the target
            drop: (item) => {
            },
      // monitor function
            collect: monitor => ({
                //isOver: !!monitor.isOver(),
               // Whether to overlap
                isOver: monitor.isOver(),
                // Whether it is possible to place
                canDrop: monitor. canDrop(),
            }),
        }),
        [deps]
  }))

Join

  • spec specification object or function to create a specification object, the key contents include:
    • accept: Required. Strings, ES6 symbols, arrays of one or function props that return one of a given component. This drop target will only respond to items produced by the specified type of drag source, for example, if you useDrag drags a checkbox, useDrop receives a checkbox as well.
    • options: Optional. an ordinary object.
    • drop(item, monitor): optional. Optional, called when a compatible item is dropped on the target;
    • collect: optional, monitoring function

In addition, there are some related content such as end(item, monitor) when dragging stops, whether to allow dragging canDrag(monitor), options, etc., which can be learned in more learning links.

  • deps

deps An array of dependencies for memory. This is similar to the built-in useMemo hook. The default is an empty array of function specifications, and an array of specifications containing object specifications.

Return value

  • arguments[0]: An object containing the attributes collected from the collect function. If collect does not define a function, it returns an empty object.
  • arguments[1]: The connector function of the drag source. This has to be attached to the draggable part of the DOM.

Written here, it can be seen that to achieve the drag effect, there are three key points. First, use Dnd to wrap the ref element we need to drag and drop, and then declare it to be dragged (drag source) and other places where it can be placed ( drop source)

Concrete implementation

The DndCheckboxGroupExample component defines drag and drop items, uses useDrag and useDrop wrappers, and calls the drag and drop index to and moveOption methods passed by the parent component to handle drag and drop.

const CHECKBOX_TYPE = 'dndCheckbox';

function DraggableCheckbox(props) {
    const ref = useRef(null);
    const to = props. index
    const {moveOption} = useContext(OptionsContext);

    const [, drag] = useDrag(
        () => ({
            type: CHECKBOX_TYPE,
            item: {index: to},
            collect: (monitor) => ({
                isDragging: monitor. isDragging()
            })
        }), [to]
    )
    const [, drop] = useDrop(() => ({
            accept: CHECKBOX_TYPE,
            drop: (item) => {
                let from = item. index;
                moveOption(from, to)
            },
            collect: monitor => ({
                isOver: !!monitor.isOver(),
            }),
        }),
        [to, moveOption]
    )

    drop(drag(ref))

    if(typeof props?.item === 'string'){
        return <span ref={ref}><Checkbox value={props.item}>{to}{props.item}</Checkbox> </span>
    }
    else {
        return <span ref={ref}><Checkbox value={props.item.value}>{to}{props.item.label}</Checkbox> </span>
    }
}
  • Bind the ref to get the component we dragged (small pit, the packaged component can’t get the ref directly, you need to put a layer of container outside)
  • Get the data we drag in the list index (to)
  • Call the moveOption (from, to) method through useDrag (this method is critical) written in the OptionsContextProvider above

passed in the component

/**
 * useCallback is used to return a function, in parent-child component parameter passing or general function encapsulation
 * The returned function a will change according to the change of b. If b has not changed, a will not be regenerated to avoid unnecessary update of the function.
 * @param {number} oldIndex - old index
 * @param {number} newIndex - new index
 * @returns {function} - returns a function
 */
const moveOption = useCallback((oldIndex, newIndex) => {
    // Get the element that needs to be moved
    const movedItem = options[oldIndex]
    // Filter out new elements based on the old index
    const sortedOptions = options. filter((item, index) => {
        return index !== oldIndex
    })
    // Insert the element that needs to be moved to the new position
    sortedOptions.splice(newIndex, 0, movedItem)
    // sort by new element
    const sortedValue = value. sort((a,b) => {
        return sortedOptions.findIndex((item)=>item.value === a) -
            sortedOptions.findIndex((item)=>item.value === b)
    })
    // update options
    onChange(sortedValue)
    // update options
    setOptions(sortedOptions)
}, [options, setOptions, value])

Lessons from pitfalls

At the beginning, the useDrop dependency of Xiaocai only wrote

So I found that the data I updated, the dragged data will become the initial default selected value, and the selected state after the selection will be canceled, and the description cannot be described. Let’s take a look at the screenshot.

It can be seen that the check box with index 2 is dragged and should be moved to the first place

However after dragging

Restored to the default (cry cry)

If you encounter a mistake, you must solve it, and then write useMemo and useEffect in a different way. The ending can be imagined. . . . I once suspected that my data was wrongly transmitted, debug, debug, and couldn’t solve it, so I went to re-read the document and found deps. I also put moveOption in the dependency to monitor and update the data in time, otherwise the data will always be restored. Defaults. . .

demo: https://github.com/Py-spj/syxDemo

More learning (reference) link:

  • React DnD
  • Drag and drop components: React-DnD usage and source code analysis – Nuggets