# Š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.
| Odluka | Preporučeni default | Zašto je važno |
|---|---|---|
| Odredište uploada | Object storage s presigned URL-ovima | Smanjuje opterećenje backenda i bolje skalira za velike datoteke |
| Način uploada | Multipart ili chunked za velike datoteke | Omogućuje retry po dijelu, pauzu/nastavak i bolji oporavak od grešaka |
| Izvor napretka | XMLHttpRequest upload progress ili fetch streaming | Treba vam stvarni napredak po datoteci da biste izgradili povjerenje |
| Identitet datoteke | Stabilan ID generiran na klijentu | Drži UI state konzistentnim kroz retry i promjene redoslijeda |
| Slojevi validacije | Klijent i server | Klijent 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:
| Status | UI tekst | Akcija korisnika |
|---|---|---|
queued | Na čekanju… | Promijeni redoslijed ili ukloni |
hashing | Priprema… | Pričekaj |
uploading | Upload 42% | Pauziraj ili otkaži |
paused | Pauzirano | Nastavi |
success | Uploadano | Pogledaj ili ukloni |
error | Neuspješno: network timeout | Pokuš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:
| Sloj | Odgovornost | React površina |
|---|---|---|
| UI komponente | Render drop zone, lista, progres, tipke | UploadDropzone, UploadList, UploadItem |
| Store stanja | Red, statusi, progres, greške, akcije | useUploadStore s reducerom |
| Upload engine | Transport, retry, chunking, pauza/nastavak | UploadManager klasa ili servis |
| API adapter | Presign URL-ovi, sesije uploada, finalizacija | uploadApi modul |
Predloženi data model#
Trebate jedan izvor istine po datoteci.
| Polje | Tip | Napomene |
|---|---|---|
id | string | Stabilan ID generiran na klijentu |
file | File | Izvorni objekt iz preglednika |
name | string | Prikazno ime, po potrebi editabilno |
size | number | Bajtovi |
type | string | MIME |
status | string | queued, uploading, error, itd. |
progress | number | 0-100 integer ili float |
error | string ili null | Poruka razumljiva korisniku |
attempts | number | Za retry logiku |
remoteKey | string ili null | Storage 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.
// 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.
// 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
buttonkoji pokrećeinput type="file" - Stavke na listi uploada imaju prave
buttonelemente 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#
| Validacija | Kako | UX korist |
|---|---|---|
| Broj datoteka | Usporedi duljinu odabira s limitom | Sprječava dugačke redove koje korisnik ne može poslati |
| Veličina datoteke | file.size | Sprječava gubitak vremena na nemoguće uploadove |
| MIME tip | file.type + provjera ekstenzije | Smanjuje server rejectove |
| Osnovni potpis | Pročitaj prve bajtove s FileReader | Hvata pogrešno označene datoteke u čestim slučajevima |
| Dimenzije slike | Napravi Image objekt | Sprječ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.”
// 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 datoteke | Veličina chunka | Napomene |
|---|---|---|
| Manje od 50 MB | Jedan zahtjev | Neka bude jednostavno |
| 50 MB do 500 MB | 8 do 16 MB | Dobar balans overheada i cijene retryja |
| Više od 500 MB | 16 do 64 MB | Manje 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ške | Retry? | Strategija |
|---|---|---|
| Network greška ili timeout | Da | Do 3 do 5 pokušaja s backoffom |
| HTTP 429 | Da | Backoff uz poštivanje Retry-After ako postoji |
| HTTP 5xx | Da | Backoff, ograničiti ukupno vrijeme retryja |
| HTTP 4xx (auth, validacija) | Ne | Brzo failanje, prikaži akciju za ispravak |
Primjer backoffa: delay = base * 2^attempt + random(0..250ms), uz maksimalni cap.
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
UploadItemunose - 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#
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:
- 1Upload bajtova u storage
- 2Potvrda 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:
| Scenarij | Kako testirati | Očekivano UX ponašanje |
|---|---|---|
| Spori upload | Browser devtools network throttling | Progres ostaje stabilan, UI ostaje responzivan |
| Prekid veze usred uploada | Uključite offline u devtools | Datoteka padne s “Pokušaj ponovno”, bez zamrzavanja UI-ja |
| Velika datoteka + refresh | Osvježite usred uploada | Ako je podržano, nastavi; ako nije, jasno objasni |
| Server odbije datoteku | Vratite 400 iz finalize | Prikaži akcijski error, ne vrtjeti retry u petlji |
| Puno datoteka | Odaberite 50+ malih datoteka | Red 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
Osnivač i senior developer u Samiodi. 8+ godina iskustva u izradi React, Next.js, Flutter i n8n rješenja za klijente diljem Europe.
Više iz kategorije Web razvoj
Sve →Upravljanje pristupom značajkama u Next.js (App Router): dozvole, prava korištenja i pristup prema naplati
Naučite obrasce za upravljanje pristupom značajkama u Next.js App Routeru: ograničite UI, server actions i API-je prema ulozi, planu, pravima korištenja i statusu Stripe pretplate uz dosljedne provjere.
Next.js + Stripe webhookovi u App Routeru: vodič za implementaciju s fokusom na pouzdanost
Vodič spreman za produkciju za Next.js Stripe webhookove u App Routeru — provjera potpisa, rad sa sirovim tijelom zahtjeva, idempotentnost, ponovni pokušaji, redoslijed događaja, Stripe CLI testiranje i rješavanje problema.
Supabase RLS + Next.js App Router: End-to-kraj autorizacija bez opasnih zamki
Praktičan vodič za 2026. za Supabase RLS s Next.js App Routerom — dizajn politika za multi-tenant, sigurni obrasci server-side provedbe i kontrolna lista za izbjegavanje curenja service role ključa, nesigurnog RPC-a i filtriranja na klijentu.
Trebate pomoć s projektom?
Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.
Povezani članci
UX obrasci u Next.js App Routeru: Error Boundaries, Loading UI i streaming kako treba
Praktičan vodič za otporan UX u Next.js App Routeru: struktura route segmenata, error.tsx, loading.tsx, not-found.tsx i Suspense streaming obrasci za djelomično renderiranje, sigurnije dohvaćanje podataka i manje pomaka layouta.
React obrasci u velikim aplikacijama: React Hook Form + Zod obrasci za složene proizvode
Najbolje prakse za React obrasce u velikim aplikacijama uz React Hook Form i Zod: validacija po shemi, višekratno upotrebljiva polja, asinkrone provjere, višekoračni tokovi, performanse, pristupačnost i obrasci integracije sa serverom/API-jem.
Rukovanje greškama u Reactu u 2026.: Error Boundaries, ponovni pokušaji i UX obrasci koji skaliraju
Praktičan, slojeviti vodič za obrasce rukovanja greškama u Reactu u 2026.: granice grešaka na razini komponente i rute, ponovni pokušaji u TanStack Query i SWR-u te UX poruke koje skaliraju kroz timove.