How "React Hooks" uses 120 lines of code to implement an interactive drag-and-drop upload component

Preface

In this article, you will learn:

  • How to rewrite existing components to React Hooks function components
  • How does useState, useEffect, useRef replace the original life cycle and Ref?
  • Four events covered by a complete drag-and-drop upload behavior: dragover, dragenter, drop, dragleave
  • How to use React Hooks to write your own UI component library.

I read this article when I visited foreign communities:

How To Implement Drag and Drop for Files in React

This article talks about the streamlined implementation of drag-and-drop upload of React, but direct translation and copying is obviously not my style.

So I rewrote it with React Hooks, with 120 lines of code except CSS.
The results are as follows:

1. Adding 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>
        );
    }
}

FilesDrag AndDrop.js (non-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'>
                //Try to pass down the files?
                <span
                    role='img'
                    aria-label='emoji'
                    className='area__icon'
                >
                    &#128526;
                </span>
            </div>
        );
    }
}

1. How to rewrite it to Hooks components?

Look at the motion chart.

2. Rewrite Components

Hooks version components belong to function components, which will be modified as follows:

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'>
            //Try to pass down the files?
            <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 };

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;
    }
  }
}

Then you can see the page:

2. Realization analysis

This paper analyses DOM operation, component reuse, event triggering, preventing default behavior, and Hooks application.

1. Operation DOM: useRef

ref attributes are needed because of the need to drag and drop files to upload and manipulate component instances.

New useRef API in React Hooks
grammar

const refContainer = useRef(initialValue);
  • useRef returns a variable ref object,.
  • Its. current attribute is initialized as the initial value passed
  • The returned object will remain throughout the life cycle of the component.
...
const drop = useRef();

return (
    <div
        ref={drop}
        className='FilesDragAndDrop'
    />
    ...
    )

2. Event triggering


Completing drag-and-drop behavior with dynamic interaction is not easy. Four event controls are needed:

  • Outside the area: dragleave, out of range
  • Zone: dragenter, used to determine whether the placement target accepts placement.
  • Intra-area movement: dragover, used to determine how feedback information is displayed to the user
  • Complete dragging (dropping): drop, allowing objects to be placed.

These four events coexist in order to prevent Web browsers from default behavior and form feedback.

3. Prevent default behavior

The code is simple:

e.preventDefault() //Prevent default behavior of events (such as opening files in browsers)
e.stopPropagation() // Prevent Event Bubble

Every event phase needs to be stopped. Why? Chestnuts:

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

If you don't stop it, it will trigger the behavior of opening the file, which is obviously not what we want to see.

4. Component internal state: useState

In addition to the basic drag-and-drop state control, drag-and-upload components should also have message reminders when files are successfully uploaded or failed to validate.
The state composition shall be:

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

Return to the following before writing the corresponding useState:

const = useState (default value);

So it became:

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

5. Need a second superimposed layer

Apart from drop events, the other three events are dynamic, and dragover events are triggered every 350 milliseconds when dragging elements.

At this point, the second ref is needed to unify the control.

So the whole `ref` is:

const drop = useRef(); // Dropping Floor
const drag = useRef(); // Dragging active layer

6. File Type and Quantity Control

When we apply components, prop needs to control the type and number of incoming components

<FilesDragAndDrop
    onUpload={this.onUpload}
    count={1}
    formats={['jpg', 'png']}
>
    <div className={classList['FilesDragAndDrop__area']}>
        //Try to pass down the files?
<span
            role='img'
            aria-label='emoji'
            className={classList['area__icon']}
        >
            &#128526;
</span>
    </div>
</FilesDragAndDrop>
  • onUpload: Drag to complete processing events
  • count: Quantity control
  • formats: 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, you can upload at most every time. ${count} Documentation.`, 'error', 2000);
        return;
    }
    if (formats && files.some((file) => !formats.some((format) => file.name.toLowerCase().endsWith(format.toLowerCase())))) {
        showMessage(`Only uploads are allowed ${formats.join(', ')}Format file`, 'error', 2000);
        return;
    }
    if (files && files.length) {
        showMessage('Successful upload!', 'success', 1000);
        props.onUpload(files);
    }
};

EndsWith is to determine the end of a string, such as "abcd". endsWith ("cd"); and // true

showMessage controls display text:

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

You need to trigger the timer to return to the initial state

7. The triggering and destruction of events in the life cycle

EventListener events need to be added to component DidMount and destroyed in component WillUnmount:

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

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

But Hooks has internal operations and corresponding useEffect to replace the two life cycles mentioned above.

useEffect example:

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

Each effect can return a cleanup function. In this way, you can put together the logic of adding and removing component WillUnmount subscriptions.

So the above can be written as follows:

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


This is too fragrant!!!

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's drop.current replaces ref's this.drop.
        drop.current.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, you can upload at most every time. ${count} Documentation.`, 'error', 2000);
            return;
        }

        if (formats && files.some((file) => !formats.some((format) => file.name.toLowerCase().endsWith(format.toLowerCase())))) {
            showMessage(`Only uploads are allowed ${formats.join(', ')}Format file`, 'error', 2000);
            return;
        }

        if (files && files.length) {
            showMessage('Successful upload!', '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' ? <>&#128546;</> : <>&#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 };

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']}>
                    //Try to pass down the files?
            <span
                        role='img'
                        aria-label='emoji'
                        className={classList['area__icon']}
                    >
                        &#128526;
            </span>
                </div>
            </FilesDragAndDrop>
        )
    }
}

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;
    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;
  }
}

Then you can get the papers and play slowly.

The Front End Exchange Group of Public Numbers for Advising and Returning Teachers

  • Add Wechat: huab119, reply: add group. Join the front-end public number exchange group.

Keywords: React emoji Attribute

Added by drak on Tue, 03 Sep 2019 13:20:30 +0300