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:
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' > 😎 </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' > 😎 </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']} > 😎 </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' ? <>😢</> : <>😘</>} </span> </div> )} {dragging && ( <div ref={drag} className={classList['FilesDragAndDrop__placeholder']} > //Please let go. <span role='img' aria-label='emoji' className={classList['area__icon']} > 😝 </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']} > 😎 </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.