preface

You will learn:

  • How do I rewrite an existing component toReact HooksFunction component
  • useState,useEffect,useRefHow to replace the original life cycle andRef.
  • A complete drag-and-upload behavior covers four events:dragover,dragenter,drop,dragleave
  • How to useReact HooksWrite your own UI component library.

Visit foreign community to see this article:

How To Implement Drag and Drop for Files in React

The article talks about a streamlined implementation of React drag-and-upload, but direct translation is clearly not my style.

I rewrote it with React Hooks, adding 120 lines excluding CSS. The effect is as follows:

1. Add a basic directory skeleton

app.js

import React from 'react';
import PropTypes from 'prop-types';

import { FilesDragAndDrop } from '../components/Common/FilesDragAndDropHook';

export default class App extends React.Component {
    static propTypes = {};

    onUpload = (files) => {
        console.log(files);
    };

    render() {
        return (
            <div>
                <FilesDragAndDrop
                    onUpload={this.onUpload}
                />
            </div>
        );
    }
}
Copy the code

FilesDragAndDrop. Js (not Hooks) :

import React from 'react'; import PropTypes from 'prop-types'; import '.. /.. /scss/components/Common/FilesDragAndDrop.scss'; export default class FilesDragAndDrop extends React.Component { static propTypes = { onUpload: PropTypes.func.isRequired, }; Render () {return (<div className='FilesDragAndDrop__area'> render() {return (<div className='FilesDragAndDrop__area'> <span role='img' aria-label='emoji' className='area__icon' > &#128526; </span> </div> ); }}Copy the code

1. How to rewrite asHooksComponents?

See the GIF:

2. Rewrite components

The Hooks component is a function component and changes the above:

import React, { useEffect, useState, useRef } from "react"; import PropTypes from 'prop-types'; import classNames from 'classnames'; import classList from '.. /.. /scss/components/Common/FilesDragAndDrop.scss'; Const FilesDragAndDrop = (props) => {return (<div className='FilesDragAndDrop__area'> <span role='img' aria-label='emoji' className='area__icon' > &#128526; </span> </div> ); } FilesDragAndDrop.propTypes = { onUpload: PropTypes.func.isRequired, children: PropTypes.node.isRequired, count: PropTypes.number, formats: PropTypes.arrayOf(PropTypes.string) } export { FilesDragAndDrop };Copy the code

FilesDragAndDrop.scss

.FilesDragAndDrop { .FilesDragAndDrop__area { width: 300px; height: 200px; padding: 50px; display: flex; align-items: center; justify-content: center; flex-flow: column nowrap; font-size: 24px; color: #555555; border: 2px #c3c3c3 dashed; border-radius: 12px; .area__icon { font-size: 64px; margin-top: 20px; }}}Copy the code

Then you can see the page:

2. Implementation analysis

Analysis from DOM manipulation, component reuse, event firing, blocking default behavior, and Hooks applications.

1. Manipulate DOM:useRef

The ref attribute is needed because you need to drag and drop file uploads and manipulate component instances.

New useRef API syntax in React Hooks

const refContainer = useRef(initialValue);
Copy the code
  • useRefReturns a mutablerefObject,.
  • its.currentProperties are initialized as passed parameters (initialValue)
  • The returned object persists throughout the life of the component.
. const drop = useRef(); return ( <div ref={drop} className='FilesDragAndDrop' /> ... )Copy the code

2. An event is triggered

Implementing drag-and-drop behavior with dynamic interaction is not simple, and requires four event controls:

  • Outside the area:dragleave, out of range
  • Area:dragenterIs used to determine whether the placement target accepts the placement.
  • Intra-area movement:dragoverTo determine what kind of feedback to display to the user
  • Complete drag (drop) :dropAllows objects to be placed.

The combination of these four events prevents the Web browser from default behavior and creates feedback.

3. Prevent default behavior

The code is simple:

E.preventdefault () // prevents the default behavior of the event (such as opening a file in a browser) e.topPropagation () // prevents the event from bubblersCopy the code

Every phase of the event needs to be blocked. Why? For example, 🌰 chestnuts:

const handleDragOver = (e) => {
    // e.preventDefault();
    // e.stopPropagation();
};
Copy the code

If not blocked, it will trigger file opening behavior, which is obviously not what we want to see.

4. Internal status of components:useState

The drag-and-drop upload component, in addition to basic drag-and-drop state control, should also have a message notification when a file has been successfully uploaded or failed authentication. The state composition should be:

state = {
    dragging: false,
    message: {
        show: false,
        text: null,
        type: null,
    },
};
Copy the code

Before the corresponding useState is written, the following formula is written by regression:

Const [property, method to manipulate property] = useState(default);Copy the code

Thus it became:

const [dragging, setDragging] = useState(false);
const [message, setMessage] = useState({ show: false, text: null, type: null });
Copy the code

5. A second stack is required

Except for the drop event, the other three events are dynamic, and the dragover event is triggered every 350 milliseconds when an element is dragged.

A second REF is needed to unify control.

So all ‘ref’ ‘is:

const drop = useRef(); // const drag = useRef(); // Drag the active layerCopy the code

6. File type and quantity control

When we apply components, prop needs to be controlled by passing in types and quantities

<FilesDragAndDrop onUpload={this.onUpload} count={1} formats={['jpg', 'PNG ']}> <div className={classList['FilesDragAndDrop__area']}> <span role='img' aria-label='emoji' className={classList['area__icon']} > &#128526; </span> </div> </FilesDragAndDrop>Copy the code
  • onUpload: Drag to finish processing the event
  • count: Quantity control
  • formats: Indicates the file type.

The corresponding component Drop internal event: handleDrop:

const handleDrop = (e) => { e.preventDefault(); e.stopPropagation(); setDragging(false) const { count, formats } = props; const files = [...e.dataTransfer.files]; If (count && count < files.length) {showMessage(' Sorry, only ${count} files can be uploaded at most each time. `, 'error', 2000); return; } if (formats && files.some((file) => ! Formats.some((format) => file.name.tolowerCase ().endswith (format.tolowercase ())))) {showMessage(' only uploads are allowed ${formats. Join (', ')} file ', 'error', 2000); return; } if (files && files.length) {showMessage(' Successfully uploaded! ', 'success', 1000); props.onUpload(files); }};Copy the code

.endswith is used to judge the end of a string, for example: “abcd”.endswith (” CD “); // true

ShowMessage controls display text:

const showMessage = (text, type, timeout) => {
    setMessage({ show: true, text, type, })
    setTimeout(() =>
        setMessage({ show: false, text: null, type: null, },), timeout);
};
Copy the code

A timer needs to be triggered to return to the initial state

7. Event triggering and destruction during the life cycle

The original EventListener event needs to be added to componentDidMount and destroyed at componentWillUnmount:

componentDidMount () {
    this.drop.addEventListener('dragover', this.handleDragOver);
}

componentWillUnmount () {
    this.drop.removeEventListener('dragover', this.handleDragOver);
}
Copy the code

Instead of the two life cycles, Hooks have internal action methods and useEffects

UseEffect example:

useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // Update only when count changesCopy the code

Each effect can return a cleanup function. This brings together the logic for adding (componentDidMount) and removing (componentWillUnmount) subscriptions.

The above can then be written as:

useEffect(() => { drop.current.addEventListener('dragover', handleDragOver); return () => { drop.current.removeEventListener('dragover', handleDragOver); }})Copy the code

It smells so good!!

3. Complete code:

FilesDragAndDropHook.js:

import React, { useEffect, useState, useRef } from "react"; import PropTypes from 'prop-types'; import classNames from 'classnames'; import classList from '.. /.. /scss/components/Common/FilesDragAndDrop.scss'; const FilesDragAndDrop = (props) => { const [dragging, setDragging] = useState(false); const [message, setMessage] = useState({ show: false, text: null, type: null }); const drop = useRef(); const drag = useRef(); UseEffect (() = > {/ / useRef drop. The current replaced the ref this. Drop by drop. The current. The addEventListener (' dragover, handleDragOver); drop.current.addEventListener('drop', handleDrop); drop.current.addEventListener('dragenter', handleDragEnter); drop.current.addEventListener('dragleave', handleDragLeave); return () => { drop.current.removeEventListener('dragover', handleDragOver); drop.current.removeEventListener('drop', handleDrop); drop.current.removeEventListener('dragenter', handleDragEnter); drop.current.removeEventListener('dragleave', handleDragLeave); } }) const handleDragOver = (e) => { e.preventDefault(); e.stopPropagation(); }; const handleDrop = (e) => { e.preventDefault(); e.stopPropagation(); setDragging(false) const { count, formats } = props; const files = [...e.dataTransfer.files]; If (count && count < files.length) {showMessage(' Sorry, only ${count} files can be uploaded at most each time. `, 'error', 2000); return; } if (formats && files.some((file) => ! Formats.some((format) => file.name.tolowerCase ().endswith (format.tolowercase ())))) {showMessage(' only uploads are allowed ${formats. Join (', ')} file ', 'error', 2000); return; } if (files && files.length) {showMessage(' Successfully uploaded! ', 'success', 1000); props.onUpload(files); }}; const handleDragEnter = (e) => { e.preventDefault(); e.stopPropagation(); e.target ! == drag.current && setDragging(true) }; const handleDragLeave = (e) => { e.preventDefault(); e.stopPropagation(); e.target === drag.current && setDragging(false) }; const showMessage = (text, type, timeout) => { setMessage({ show: true, text, type, }) setTimeout(() => setMessage({ show: false, text: null, type: null, },), timeout); }; return ( <div ref={drop} className={classList['FilesDragAndDrop']} > {message.show && ( <div className={classNames( classList['FilesDragAndDrop__placeholder'], classList[`FilesDragAndDrop__placeholder--${message.type}`], )} > {message.text} <span role='img' aria-label='emoji' className={classList['area__icon']} > {message.type === 'error' ? < > the & # 128546; < / a > : < > the & # 128536; </>} </span> </div>)} {dragging && (<div ref={drag} className={classList['FilesDragAndDrop__placeholder']} > Please let go <span  role='img' aria-label='emoji' className={classList['area__icon']} > &#128541; </span> </div> )} {props.children} </div> ); } FilesDragAndDrop.propTypes = { onUpload: PropTypes.func.isRequired, children: PropTypes.node.isRequired, count: PropTypes.number, formats: PropTypes.arrayOf(PropTypes.string) } export { FilesDragAndDrop };Copy the code

App.js:

import React, { Component } from 'react'; import { FilesDragAndDrop } from '.. /components/Common/FilesDragAndDropHook'; import classList from '.. /scss/components/Common/FilesDragAndDrop.scss'; export default class App extends Component { onUpload = (files) => { console.log(files); }; render () { return ( <FilesDragAndDrop onUpload={this.onUpload} count={1} formats={['jpg', 'png', 'GIF ']}> <div className={classList['FilesDragAndDrop__area']}> <span role='img' aria-label='emoji' className={classList['area__icon']} > &#128526; </span> </div> </FilesDragAndDrop> ) } }Copy the code

FilesDragAndDrop.scss:

.FilesDragAndDrop { position: relative; .FilesDragAndDrop__placeholder { position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; z-index: 9999; display: flex; align-items: center; justify-content: center; flex-flow: column nowrap; background-color: #e7e7e7; border-radius: 12px; color: #7f8e99; font-size: 24px; opacity: 1; text-align: center; The line - height: 1.4; &.FilesDragAndDrop__placeholder--error { background-color: #f7e7e7; color: #cf8e99; } &.FilesDragAndDrop__placeholder--success { background-color: #e7f7e7; color: #8ecf99; } .area__icon { font-size: 64px; margin-top: 20px; } } } .FilesDragAndDrop__area { width: 300px; height: 200px; padding: 50px; display: flex; align-items: center; justify-content: center; flex-flow: column nowrap; font-size: 24px; color: #555555; border: 2px #c3c3c3 dashed; border-radius: 12px; .area__icon { font-size: 64px; margin-top: 20px; }}Copy the code

Then you can get the files and play with them…

❤️ Read three things

If you find this article inspiring, I’d like to invite you to do me three small favors:

  1. Like, so that more people can see this content (collection does not like, is a rogue -_-)
  2. Follow the public account “front end persuader” to share original knowledge from time to time.
  3. Look at other articles as well
  • Design patterns that you inadvertently use (I) – Creative patterns
  • [” King of data Visualization library “D3.js speed hand to Vue application

] (juejin. Cn/post / 684490…).

  • “True ® Full Stack Road” Back-end guide to Web front-end development
  • “Vue Practice” 5 minutes for a Vue CLI plug-in
  • “Vue practices” arm your front-end projects
  • “Intermediate and advanced front-end interview” JavaScript handwritten code unbeatable secrets
  • The answer to the Vue question “Learn from source” that no interviewer knows
  • “Learn from the source” Vue source JS operations
  • The “Vue Practice” project upgrades vuE-CLI3 to correct posture
  • Why do you never understand JavaScript scope chains?