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 { 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.

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