typescript
71 lines · 9 steps
A drag-and-drop file upload zone in React
A React component that accepts dropped files, validates type and size, and tracks nested drag events with a ref counter.
Explained by
highlit
1import { useCallback, useRef, useState } from "react";
2
3type UploadZoneProps = {
4 accept?: string[];
5 maxSizeBytes?: number;
6 onFiles: (files: File[]) => void;
7};
8
9export function UploadZone({ accept, maxSizeBytes = 10 * 1024 * 1024, onFiles }: UploadZoneProps) {
10 const [dragging, setDragging] = useState(false);
11 const [error, setError] = useState<string | null>(null);
12 const dragDepth = useRef(0);
13
14 const validate = useCallback(
15 (files: File[]) => {
16 for (const file of files) {
17 if (accept && !accept.some((type) => file.type === type || file.name.endsWith(type))) {
18 return `\"${file.name}\" is not an accepted file type`;
19 }
20 if (file.size > maxSizeBytes) {
21 return `\"${file.name}\" exceeds ${Math.round(maxSizeBytes / 1024 / 1024)}MB`;
22 }
23 }
24 return null;
25 },
26 [accept, maxSizeBytes],
27 );
28
29 const handleDrop = useCallback(
30 (event: React.DragEvent<HTMLDivElement>) => {
31 event.preventDefault();
32 dragDepth.current = 0;
33 setDragging(false);
34
35 const files = Array.from(event.dataTransfer.files);
36 if (files.length === 0) return;
37
38 const validationError = validate(files);
39 if (validationError) {
40 setError(validationError);
41 return;
42 }
43 setError(null);
44 onFiles(files);
45 },
46 [validate, onFiles],
47 );
48
49 return (
50 <div
51 onDragEnter={(e) => {
52 e.preventDefault();
53 dragDepth.current += 1;
54 setDragging(true);
55 }}
56 onDragOver={(e) => e.preventDefault()}
57 onDragLeave={(e) => {
58 e.preventDefault();
59 dragDepth.current -= 1;
60 if (dragDepth.current === 0) setDragging(false);
61 }}
62 onDrop={handleDrop}
63 className={dragging ? "dropzone dropzone--active" : "dropzone"}
64 role="button"
65 tabIndex={0}
66 >
67 <p>{dragging ? "Release to upload" : "Drag files here to upload"}</p>
68 {error && <p className="dropzone__error">{error}</p>}
69 </div>
70 );
71}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A ref counter cleanly solves the flickering caused by dragenter/dragleave firing on nested child elements.
- 2Validating files before invoking the callback keeps invalid input from ever reaching the parent.
- 3Memoizing handlers with useCallback keeps their identity stable across renders while capturing the right dependencies.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
typescript
import { Injectable, Scope, Inject, NotFoundException } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Request } from 'express'; import { DataSource } from 'typeorm';
Per-tenant database connections in NestJS
multi-tenancy
connection-pooling
dependency-injection
Advanced
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-drag-and-drop-file-upload-zone-in-react-explained-typescript-1766/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.