React - Forms: File Uploads & Progress

Overview

Estimated time: 20–30 minutes

Learn to accept files, preview images, and present an upload progress bar. We simulate the upload to avoid CORS issues, but the UI and control flow match production patterns.

Try it: Image preview + simulated upload

View source
const { useEffect, useRef, useState } = React;
function Uploader(){
  const [file, setFile] = useState(null);
  const [preview, setPreview] = useState('');
  const [progress, setProgress] = useState(0);
  const [uploading, setUploading] = useState(false);
  const tRef = useRef(null);
  useEffect(() => {
    if (!file) { setPreview(''); return; }
    if (file && file.type.startsWith('image/')){
      const reader = new FileReader();
      reader.onload = () => setPreview(String(reader.result||''));
      reader.readAsDataURL(file);
    } else {
      setPreview('');
    }
  }, [file]);
  function onFile(e){ setFile(e.target.files && e.target.files[0] ? e.target.files[0] : null); setProgress(0); }
  function reset(){ setFile(null); setPreview(''); setProgress(0); setUploading(false); if (tRef.current) clearInterval(tRef.current); }
  function simulateUpload(){
    if (!file) return;
    setUploading(true);
    let p = 0;
    tRef.current = setInterval(() => {
      p += Math.max(5, Math.floor(Math.random()*12));
      if (p >= 100){ p = 100; clearInterval(tRef.current); tRef.current = null; setUploading(false); }
      setProgress(p);
    }, 200);
  }
  return (
    <div>
      <input type="file" accept="image/*" onChange={onFile} />
      {file && <div style={{marginTop:8}}>
        <div><strong>File:</strong> {file.name} ({Math.round(file.size/1024)} KB)</div>
        {preview && <img src={preview} alt="preview" style={{marginTop:8, maxWidth:'100%', height:'auto', border:'1px solid var(--border)', borderRadius:8}} />}
        <div style={{marginTop:8, display:'flex', gap:8}}>
          <button onClick={simulateUpload} disabled={uploading}>{uploading ? 'Uploading...' : 'Upload'}</button>
          <button onClick={reset} disabled={uploading}>Reset</button>
        </div>
        <div style={{marginTop:8, height:10, background:'rgba(148,163,184,.25)', borderRadius:6, overflow:'hidden'}} aria-label="progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow={progress}>
          <div style={{width:progress+'%', height:'100%', background:'linear-gradient(90deg,#60a5fa,#93c5fd)'}} />
        </div>
      </div>}
    </div>
  );
}
ReactDOM.createRoot(document.getElementById('try-upload')).render(<Uploader />);

Syntax primer

  • FileReader to preview images client-side.
  • Always reset progress and clear intervals on cancel/reset.

Common pitfalls

  • Not validating file type/size (add checks for production).
  • Forgetting to clear simulated timers on unmount/reset.

Checks for Understanding

  1. Why is it important to clear timers/intervals on unmount/reset?
  2. What accessibility attributes help convey progress to assistive tech?
Show answers
  1. To prevent memory leaks and stray updates after the component is gone.
  2. aria-label for the progressbar and aria-valuenow/min/max to announce progress.

Exercises

  1. Validate file size/type before allowing upload and show inline errors.
  2. Simulate cancel/pause/resume and ensure intervals are managed correctly.
  3. Add drag-and-drop support and keyboard-accessible focus styles.