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 { InjectionToken, inject, Provider, isDevMode } from '@angular/core'; import { WINDOW } from './window.token'; export interface AnalyticsConfig {
Layered config with an Angular InjectionToken
dependency-injection
configuration
factory-provider
Intermediate
8 steps
typescript
import { useState, useEffect, useRef, useCallback } from "react"; interface Suggestion { id: string;
A debounced autocomplete hook in React
debounce
custom-hooks
abortcontroller
Advanced
7 steps
javascript
class LyricsSync { constructor(audio, container, lines) { this.audio = audio; this.container = container;
Building a synced lyrics highlighter
binary-search
dom-manipulation
event-handling
Intermediate
9 steps
typescript
import { Body, Controller, Ip, Post, UnauthorizedException } from '@nestjs/common'; import { Throttle, ThrottlerGuard } from '@nestjs/throttler'; import { UseGuards } from '@nestjs/common'; import { AuthService } from './auth.service';
Rate-limiting an auth flow in NestJS
rate-limiting
authentication
guards
Intermediate
8 steps
typescript
import { Component, computed, DestroyRef, inject, input, output, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { interval, map, takeWhile } from 'rxjs';
How a signal-driven countdown works in Angular
signals
reactivity
rxjs
Intermediate
8 steps
typescript
type TokenType = "keyword" | "string" | "comment" | "number" | "text"; interface Token { type: TokenType;
How a regex tokenizer highlights code
tokenizer
regex
lexing
Intermediate
10 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.