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

Walkthrough

Space play step click any line
Three takeaways
  1. 1A ref counter cleanly solves the flickering caused by dragenter/dragleave firing on nested child elements.
  2. 2Validating files before invoking the callback keeps invalid input from ever reaching the parent.
  3. 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.

A drag-and-drop file upload zone in React — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code