Web razvoj
ReactUXUpload datotekaPristupačnostNext.jsS3Cloudflare R2Performanse

UX obrasci za upload datoteka u Reactu: drag-and-drop, napredak, validacija i oporavak od grešaka

AO
Adrijan Omićević
·15 min čitanja

# Što ćete izgraditi i zašto je to važno#

Dobar React UX za upload datoteka nije samo drop zona i spinner. To je sustav koji pomaže korisnicima uspjeti na lošim mrežama, s velikim datotekama i u stvarnim situacijama poput uploada pogrešnog formata ili zatvaranja taba usred uploada.

Ovaj vodič pokriva provjerene UI i inženjerske obrasce: drag-and-drop koji je stvarno pristupačan, napredak po datoteci, validaciju na klijentu, optimistični UI, otporne ponovne pokušaje te napredne značajke pouzdanosti poput chunkinga i pauze/nastavka. Dobit ćete i arhitekturu komponenti koju možete primijeniti kroz različite proizvode.

ℹ️ Napomena: Korisnici procjenjuju upload flow po predvidljivosti. Ako UI ne može objasniti što se događa i što je sljedeći korak, pretpostavit će da je aplikacija pokvarena, čak i kad je backend u redu.

# Preduvjeti i temeljne odluke#

Prije nego što dizajnirate UI, odlučite kako upload radi end-to-end. Isti UI može biti iznad različitih implementacija, ali morate jasno znati ograničenja.

OdlukaPreporučeni defaultZašto je važno
Odredište uploadaObject storage s presigned URL-ovimaSmanjuje opterećenje backenda i bolje skalira za velike datoteke
Način uploadaMultipart ili chunked za velike datotekeOmogućuje retry po dijelu, pauzu/nastavak i bolji oporavak od grešaka
Izvor napretkaXMLHttpRequest upload progress ili fetch streamingTreba vam stvarni napredak po datoteci da biste izgradili povjerenje
Identitet datotekeStabilan ID generiran na klijentuDrži UI state konzistentnim kroz retry i promjene redoslijeda
Slojevi validacijeKlijent i serverKlijent poboljšava UX, server osigurava sigurnost

Ako implementirate presigned upload (AWS S3 ili Cloudflare R2), uparite ovaj post s Next.js file uploads with S3 or R2 using presigned URLs. Tamo su objašnjeni backend handshake obrasci koji omogućuju otporan UI.

# UX obrasci koji sprječavaju “rage click”#

Obrazac 1: Drop zona neka bude opcionalna, ne obavezna#

Drag-and-drop je praktičan za korisnike miša, ali nije univerzalan. Uvijek pružite eksplicitnu tipku “Odaberi datoteke” koja otvara file picker.

Pouzdan raspored je:

  • Površina drop zone
  • Tipka za otvaranje file pickera
  • Inline tekst s ograničenjima
  • Lista odabranih datoteka s akcijama po datoteci

To odmah smanjuje broj upita podršci uzrokovanih korisnicima na touch uređajima ili korisnicima koji ne mogu koristiti drag-and-drop.

Obrazac 2: Prikažite ograničenja prije nego korisnik pogriješi#

Stavite ograničenja direktno unutar drop zone:

  • Maksimalna veličina datoteke
  • Prihvaćeni formati
  • Maksimalan broj datoteka
  • Pravila imenovanja (ako je relevantno)

Primjer teksta koji dobro funkcionira: “PDF, PNG, JPG. Do 10 datoteka, do 25 MB svaka.”

💡 Savjet: Ako server nameće strogu granicu poput 25 MB, preslikajte je u UI i validirajte pri odabiru. Korisnici prihvaćaju ograničenja kada su unaprijed upozoreni.

Obrazac 3: Status po datoteci, ne jedan globalni spinner#

Globalni spinner ima smisla samo kada se upload radi za točno jednu datoteku i ne može se paralelizirati. Kod više datoteka trebate status po datoteci jer korisnici razmišljaju u stavkama, ne u ukupnom zbroju.

Uobičajeni statusi koji se dobro mapiraju na UI:

StatusUI tekstAkcija korisnika
queuedNa čekanju…Promijeni redoslijed ili ukloni
hashingPriprema…Pričekaj
uploadingUpload 42%Pauziraj ili otkaži
pausedPauziranoNastavi
successUploadanoPogledaj ili ukloni
errorNeuspješno: network timeoutPokušaj ponovno

Obrazac 4: Oprezno s optimističnim UI-jem#

Optimistični UI znači da se stavka odmah prikaže na listi, čak i prije nego upload krene. To je dobar UX, ali mora ostati iskren:

  • Datoteka se odmah pojavljuje kao queued
  • Ne broji se kao “uploadano” dok nije potvrđeno
  • Ako ne uspije, stavka ostaje vidljiva s jasnom akcijom oporavka

Optimistični UI postaje opasan kada prikažete “Uspješno” prije nego server potvrdi da objekt postoji ili prije nego se završi korak finalizacije.

Za šire obrasce grešaka i retryja u React UI-ju pogledajte React frontend error handling patterns: toasts, boundaries, retries.

# Arhitektura komponenti za skalabilan upload UI#

Najveći problem održavanja je miješanje UI-ja i transport logike u jednoj komponenti. Držite ih odvojeno i učinite upload engine testabilnim.

Praktična arhitektura:

SlojOdgovornostReact površina
UI komponenteRender drop zone, lista, progres, tipkeUploadDropzone, UploadList, UploadItem
Store stanjaRed, statusi, progres, greške, akcijeuseUploadStore s reducerom
Upload engineTransport, retry, chunking, pauza/nastavakUploadManager klasa ili servis
API adapterPresign URL-ovi, sesije uploada, finalizacijauploadApi modul

Predloženi data model#

Trebate jedan izvor istine po datoteci.

PoljeTipNapomene
idstringStabilan ID generiran na klijentu
fileFileIzvorni objekt iz preglednika
namestringPrikazno ime, po potrebi editabilno
sizenumberBajtovi
typestringMIME
statusstringqueued, uploading, error, itd.
progressnumber0-100 integer ili float
errorstring ili nullPoruka razumljiva korisniku
attemptsnumberZa retry logiku
remoteKeystring ili nullStorage key nakon presign/finalize

Store i akcije (reducer obrazac)#

To čini UI determinističkim i lako debugabilnim. Neka akcije budu eksplicitne: ADD_FILES, REMOVE_FILE, SET_PROGRESS, SET_STATUS, SET_ERROR, RETRY_FILE, PAUSE_FILE, RESUME_FILE.

TypeScript
// useUploadStore.ts
type UploadStatus = 'queued' | 'uploading' | 'paused' | 'success' | 'error';
 
type UploadItem = {
  id: string;
  file: File;
  status: UploadStatus;
  progress: number;
  error: string | null;
  attempts: number;
};
 
type Action =
  | { type: 'ADD_FILES'; items: UploadItem[] }
  | { type: 'SET_PROGRESS'; id: string; progress: number }
  | { type: 'SET_STATUS'; id: string; status: UploadStatus }
  | { type: 'SET_ERROR'; id: string; error: string | null };
 
export function uploadReducer(state: UploadItem[], action: Action): UploadItem[] {
  switch (action.type) {
    case 'ADD_FILES':
      return [...action.items, ...state];
    case 'SET_PROGRESS':
      return state.map(x => (x.id === action.id ? { ...x, progress: action.progress } : x));
    case 'SET_STATUS':
      return state.map(x => (x.id === action.id ? { ...x, status: action.status } : x));
    case 'SET_ERROR':
      return state.map(x => (x.id === action.id ? { ...x, error: action.error } : x));
    default:
      return state;
  }
}

# Drag-and-drop UX koji radi (i pristupačan je)#

Drag-and-drop često izađe “polovično”: ne radi dobro za korisnike tipkovnice i čitače ekrana. Rješenje je tretirati ga kao progressive enhancement.

Checklist za pristupačnu drop zonu#

  • Osigurajte vidljivu tipku koja pokreće native file picker
  • Omogućite fokusiranje drop zone tipkovnicom
  • Osigurajte jasan fokus ring
  • Izložite upute i ograničenja kroz pristupačan tekst
  • Najavljujte promjene statusa kroz aria-live regiju
  • Podržite paste-to-upload ako odgovara proizvodu

Za detaljniji praktični popis koristite React accessibility a11y checklist: ARIA, keyboard, focus.

Stanja povlačenja: prikazujte samo ono što možete garantirati#

Korisnici drag highlight interpretiraju kao obećanje. Prikažite stanje “Spustite datoteke za upload” samo kada ste sigurni da je pokazivač iznad drop targeta i da će drop uspjeti.

Izbjegnite i čest problem: ugniježđeni elementi okidaju drag leave/enter događaje i uzrokuju titranje (flicker). Pratite drag dubinu.

TypeScript
// dragDepth pattern to prevent flicker
let dragDepth = 0;
 
function onDragEnter(e: DragEvent) {
  e.preventDefault();
  dragDepth += 1;
  // set isDragging true
}
 
function onDragLeave(e: DragEvent) {
  e.preventDefault();
  dragDepth -= 1;
  if (dragDepth <= 0) {
    dragDepth = 0;
    // set isDragging false
  }
}
 
function onDrop(e: DragEvent) {
  e.preventDefault();
  dragDepth = 0;
  // set isDragging false
  // handle files
}

Ponašanje za tipkovnicu i čitače ekrana#

Sama drop zona ne mora glumiti tipku. Najsigurniji obrazac je:

  • Drop zona je regija koju korisnici mogu fokusirati
  • Unutar nje je pravi button koji pokreće input type="file"
  • Stavke na listi uploada imaju prave button elemente za uklanjanje, retry, pauzu, nastavak
  • Ažuriranja statusa se najavljuju u “polite” live regiji

⚠️ Upozorenje: Nemojte se oslanjati samo na placeholder tekst ili promjenu boje za komunikaciju grešaka ili stanja “drag active”. Koristite ikone uz tekst i osigurajte da kontrast boja zadovoljava WCAG AA. Ovo je čest razlog neusklađenosti.

# Validacija na klijentu koja poboljšava UX (bez obmane)#

Validacija na klijentu treba spriječiti očite greške rano. Nikad ne smije biti jedina linija obrane.

Što validirati na klijentu#

ValidacijaKakoUX korist
Broj datotekaUsporedi duljinu odabira s limitomSprječava dugačke redove koje korisnik ne može poslati
Veličina datotekefile.sizeSprječava gubitak vremena na nemoguće uploadove
MIME tipfile.type + provjera ekstenzijeSmanjuje server rejectove
Osnovni potpisPročitaj prve bajtove s FileReaderHvata pogrešno označene datoteke u čestim slučajevima
Dimenzije slikeNapravi Image objektSprječava server-side resize failure

Primjer: blokiranje datoteke od 200 MB kada je limit 25 MB može uštedjeti korisniku minute na sporijim upload brzinama. Mnoge kućne veze i dalje imaju manje od 20 Mbps upload, što je oko 2.5 MB u sekundi u idealnim uvjetima. To znači da upload od 200 MB lako traje 80 sekundi prije overheada.

UI obrazac za validaciju: inline greške po datoteci#

Kad datoteka padne validaciju, zadržite je na listi s porukom greške i akcijom uklanjanja. Nemojte je tiho odbaciti.

Primjeri poruka koje smanjuju zbunjenost:

  • “Datoteka ima 41 MB. Maksimalno je 25 MB.”
  • “Nepodržani tip. Dozvoljeno: PDF, PNG, JPG.”
  • “Slika mora biti najmanje 1200 × 800 piksela.”
TypeScript
// simple validation helper
export function validateFile(file: File) {
  const maxBytes = 25 * 1024 * 1024;
  const allowed = ['image/png', 'image/jpeg', 'application/pdf'];
 
  if (file.size > maxBytes) return `File is too large. Max is 25 MB.`;
  if (!allowed.includes(file.type)) return `Unsupported type. Upload PNG, JPG, or PDF.`;
  return null;
}

# Povratne informacije o napretku: točne, smirene i korisne#

Progres je signal povjerenja. Također je prvo mjesto gdje korisnici primijete bugove.

Kad god možete, koristite determinate progres#

Ako možete izračunati poslane bajtove, prikažite determinate progress bar. Indeterminate spinnere čuvajte za korake gdje progres nije poznat, poput “priprema uploada” ili “finalizacija”.

Kod presigned URL-ova, XMLHttpRequest i dalje daje najšire podržane upload progress eventove. Fetch-based upload progres je moguć u modernim preglednicima, ali razlike u podršci mogu zakomplicirati produkcijsko ponašanje.

Dodajte sekundarne informacije koje smanjuju tjeskobu#

Dobar red uploada često uključuje:

  • Postotak
  • Poslane bajtove i ukupno
  • Trenutnu brzinu i procijenjeno preostalo vrijeme (kad se stabilizira)

Nemojte prikazivati ETA dok nemate stabilan prozor uzorka; inače “skače” i povećava nelagodu.

# Otporni inženjerski obrasci za upload#

Chunking i multipart: pouzdanost i bolji oporavak#

Chunking je UX značajka, ne samo infrastrukturna. Ako datoteka od 1.5 GB padne na 95% zbog kratkog prekida, upload u jednom zahtjevu kreće ispočetka. Uz multipart ponovo uploadate samo dijelove koji nedostaju.

Preporučene veličine chunkova:

Veličina datotekeVeličina chunkaNapomene
Manje od 50 MBJedan zahtjevNeka bude jednostavno
50 MB do 500 MB8 do 16 MBDobar balans overheada i cijene retryja
Više od 500 MB16 do 64 MBManje dijelova, manje overheada

Pauza i nastavak: što stvarno zahtijeva#

Pauza i nastavak ne mogu biti “lažni” UI prekidač. To zahtijeva:

  • Mogućnost otkazivanja (cancellable) requesta u letu po chunku
  • Persistanu listu dovršenih chunkova
  • Strategiju nastavka koja traži preostale presigned URL-ove ili ponovno koristi ID multipart sessiona

Ako podržavate zatvaranje taba i nastavak kasnije, trebate i perzistenciju u IndexedDB ili localStorage te server-side sesiju koja ostaje važeća.

🎯 Ključni zaključak: Pauza i nastavak su vjerodostojni samo kada korisnik može pauzirati, osvježiti stranicu i nastaviti bez povratka na nulu.

Strategije retryja koje ne kažnjavaju korisnike#

Retry treba biti po datoteci i idealno po chunku. Koristite exponential backoff s jitterom kako biste izbjegli thundering herd tijekom ispada.

Praktična politika:

Tip greškeRetry?Strategija
Network greška ili timeoutDaDo 3 do 5 pokušaja s backoffom
HTTP 429DaBackoff uz poštivanje Retry-After ako postoji
HTTP 5xxDaBackoff, ograničiti ukupno vrijeme retryja
HTTP 4xx (auth, validacija)NeBrzo failanje, prikaži akciju za ispravak

Primjer backoffa: delay = base * 2^attempt + random(0..250ms), uz maksimalni cap.

TypeScript
export async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxAttempts = 5,
  baseMs = 400,
) {
  let attempt = 0;
  while (true) {
    try {
      return await fn();
    } catch (err) {
      attempt += 1;
      if (attempt >= maxAttempts) throw err;
      const jitter = Math.floor(Math.random() * 250);
      const delay = Math.min(8000, baseMs * 2 ** (attempt - 1) + jitter);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

UX oporavka od greške: učinite sljedeći korak očitim#

Kad nešto padne, UI treba odmah odgovoriti na tri pitanja:

  • Što se dogodilo
  • Što mogu sada napraviti
  • Hoću li izgubiti napredak

Dobar red greške uključuje:

  • Jasnu oznaku: “Upload nije uspio”
  • Razlog: “Izgubljena mrežna veza”
  • Primarnu akciju: “Pokušaj ponovno”
  • Sekundarnu akciju: “Ukloni”
  • Ako je chunked: “Nastavlja se od 68 posto”

Izbjegavajte ispis sirovih server poruka. Mapirajte tehničke greške u tekst siguran za korisnika, a tehničke detalje zadržite za logove.

# Sve zajedno: Upload Manager i integracija s UI-jem#

Odgovornosti Upload Managera#

Vaš UploadManager treba:

  • Prihvaćati UploadItem unose
  • Pokretati upload s podesivom konkurentnošću
  • Emitirati progress eventove
  • Podržavati cancel i pause
  • Po potrebi perzistirati stanje
  • Koordinirati multipart finalize poziv

Default za konkurentnost: 2 do 4 paralelna uploada obično je sigurnije od 10. Previše paralelizma može zasićiti uplink i sve usporiti, posebno na mobitelu.

Jednostavan obrazac kontrole konkurentnosti#

TypeScript
export async function runPool<T>(
  tasks: Array<() => Promise<T>>,
  concurrency = 3,
) {
  const results: T[] = [];
  const executing = new Set<Promise<void>>();
 
  for (const task of tasks) {
    const p = (async () => {
      const r = await task();
      results.push(r);
    })().then(() => void 0);
 
    executing.add(p);
    p.finally(() => executing.delete(p));
 
    if (executing.size >= concurrency) await Promise.race(executing);
  }
 
  await Promise.all(executing);
  return results;
}

To drži UI responzivnim i potrošnju mreže stabilnom.

# Validacija, optimizam i finalizacija: mentalni model “two-phase commit”#

Ako uploadate direktno u object storage, obično imate proces u dva koraka:

  1. 1
    Upload bajtova u storage
  2. 2
    Potvrda ili finalizacija na backendu i kreiranje app-level zapisa

UX implikacije:

  • Korisnik treba vidjeti “Uploadano, finalizira se…” nakon što su bajtovi poslani
  • “Uspješno” se smije prikazati tek nakon što backend potvrdi da zapis postoji
  • Ako finalizacija ne uspije, UI treba ponuditi retry finalizacije, ne ponovni upload

Ova razlika smanjuje nepotrebnu potrošnju bandwidtha i čini retry “instantnim”.

# Testiranje upload UX-a kao produkt tim#

Nemojte testirati samo sretne putanje. Testirajte scenarije na koje korisnici stvarno nailaze:

ScenarijKako testiratiOčekivano UX ponašanje
Spori uploadBrowser devtools network throttlingProgres ostaje stabilan, UI ostaje responzivan
Prekid veze usred uploadaUključite offline u devtoolsDatoteka padne s “Pokušaj ponovno”, bez zamrzavanja UI-ja
Velika datoteka + refreshOsvježite usred uploadaAko je podržano, nastavi; ako nije, jasno objasni
Server odbije datotekuVratite 400 iz finalizePrikaži akcijski error, ne vrtjeti retry u petlji
Puno datotekaOdaberite 50+ malih datotekaRed ostaje upotrebljiv i dobro se scrolla

Mjerljiv cilj: korisnici bi uvijek trebali imati vidljivu akciju oporavka u jednom kliku za svaki neuspjeh koji je moguće oporaviti.

# Ključne poruke#

  • Dizajnirajte React UX za upload oko statusa po datoteci i akcija oporavka, ne oko globalnih spinnera.
  • Odvojite UI, state i transport logiku kako bi chunking, pauza/nastavak i retry ostali testabilni i održivi.
  • Validirajte rano u pregledniku veličinu, tip i broj datoteka, ali neka server validacija ostane izvor istine.
  • Koristite multipart chunking za velike datoteke kako bi se kod grešaka ponovno uploadali samo nedostajući dijelovi, što omogućuje vjerodostojan pause/resume.
  • Implementirajte retry politike prema klasi greške s exponential backoffom i jitterom te greške prikažite kao akcijska UI stanja.
  • Učinite drag-and-drop pristupačnim po defaultu uz podršku tipkovnice, jasne stilove fokusa i live najave statusa.

# Zaključak#

Otporan upload flow je konkurentska prednost: smanjuje odustajanja, smanjuje volumen podrške i gradi povjerenje na nepouzdanim mrežama. Krenite tako da isporučite status po datoteci i retry UI, a zatim dodajte chunking i pause/resume tamo gdje veličina datoteka i kontekst korisnika opravdavaju složenost.

Ako želite da Samioda implementira production-grade sustav uploada s presigned URL-ovima, multipart uploadima i dotjeranim, pristupačnim React UI-jem, kontaktirajte nas putem Samioda razvoj web i mobilnih aplikacija i odredit ćemo najbrži put do pouzdanog upload iskustva.

FAQ

Share
A
Adrijan OmićevićOsnivač i senior developer

Osnivač i senior developer u Samiodi. 8+ godina iskustva u izradi React, Next.js, Flutter i n8n rješenja za klijente diljem Europe.

Trebate pomoć s projektom?

Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.