DEV Community

toolzip
toolzip

Posted on

The Complete Guide to File Handling in the Browser

 Building a browser-based file tool means understanding the browser's file APIs. There are more of them than you'd expect, and they interop in non-obvious ways.

The File Object

A File object is what you get from <input type="file"> or a drag-and-drop event. It extends Blob.

const file = input.files[0];

console.log(file.name);     // "photo.jpg"
console.log(file.size);     // bytes: 2457600
console.log(file.type);     // "image/jpeg"
console.log(file.lastModified); // timestamp
Enter fullscreen mode Exit fullscreen mode

Reading File Content

You have four ways to read a file. Each returns a different format.

As Text

const text = await file.text();
// Use for: JSON, CSV, markdown, source code
Enter fullscreen mode Exit fullscreen mode

As ArrayBuffer

const buffer = await file.arrayBuffer();
// Use for: binary files, passing to WASM libraries
// ArrayBuffer is a fixed-length binary buffer
Enter fullscreen mode Exit fullscreen mode

As Data URL (Base64)

// Old way (callback-based)
const reader = new FileReader();
reader.onload = (e) => {
  const dataURL = e.target.result;
  // "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
};
reader.readAsDataURL(file);

// For use as image src, email attachments
Enter fullscreen mode Exit fullscreen mode

As Object URL

const url = URL.createObjectURL(file);
// "blob:https://example.com/a1b2c3d4..."

// Use as src for <img>, <video>, <audio>
img.src = url;

// IMPORTANT: Revoke when done to free memory
URL.revokeObjectURL(url);
Enter fullscreen mode Exit fullscreen mode

Drag and Drop

const dropZone = document.getElementById("drop-zone");

dropZone.addEventListener("dragover", (e) => {
  e.preventDefault(); // Required to allow drop
  dropZone.classList.add("active");
});

dropZone.addEventListener("dragleave", () => {
  dropZone.classList.remove("active");
});

dropZone.addEventListener("drop", (e) => {
  e.preventDefault();
  dropZone.classList.remove("active");

  const files = Array.from(e.dataTransfer.files);
  handleFiles(files);
});
Enter fullscreen mode Exit fullscreen mode

File Type Validation

Don't rely on file.type alone — it's derived from the filename extension, which can be wrong or missing.

function validateFile(file, allowedTypes) {
  // Check MIME type
  if (!allowedTypes.includes(file.type)) {
    return false;
  }

  // Also check extension
  const ext = file.name.split(".").pop().toLowerCase();
  const allowedExts = {
    "image/jpeg": ["jpg", "jpeg"],
    "image/png": ["png"],
    "image/webp": ["webp"],
    "application/pdf": ["pdf"],
  };

  return allowedExts[file.type]?.includes(ext) ?? false;
}
Enter fullscreen mode Exit fullscreen mode

For true validation, read the file's magic bytes:

async function isPDF(file) {
  const buffer = await file.slice(0, 4).arrayBuffer();
  const bytes = new Uint8Array(buffer);
  // PDF magic bytes: %PDF = 0x25 0x50 0x44 0x46
  return bytes[0] === 0x25 && bytes[1] === 0x50 
      && bytes[2] === 0x44 && bytes[3] === 0x46;
}
Enter fullscreen mode Exit fullscreen mode

Blob — The Output Format

When you've processed a file and want to offer it for download, you'll create a Blob.

// Create Blob from string
const blob = new Blob(["Hello, world!"], { type: "text/plain" });

// Create Blob from ArrayBuffer (e.g., from WASM processing)
const blob = new Blob([arrayBuffer], { type: "video/mp4" });

// Create Blob from canvas
canvas.toBlob((blob) => {
  // blob is ready
}, "image/jpeg", 0.85);
Enter fullscreen mode Exit fullscreen mode

Triggering Downloads

function downloadBlob(blob, filename) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;

  // Must append to document for Firefox
  document.body.appendChild(a);
  a.click();

  // Cleanup
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
}
Enter fullscreen mode Exit fullscreen mode

Memory Management

This is where most file tool implementations leak memory.

// Every createObjectURL needs a matching revokeObjectURL
const url = URL.createObjectURL(blob);
img.src = url;

img.onload = () => {
  // Revoke AFTER the image has loaded
  URL.revokeObjectURL(url);
};

// Bad: revoke before load completes
URL.revokeObjectURL(url); // ❌ image might not display
img.src = url;
Enter fullscreen mode Exit fullscreen mode

For long-lived URLs (preview images that stay on screen), track them and revoke on component unmount:

// React hook
function useObjectURL(blob) {
  const [url, setUrl] = useState(null);

  useEffect(() => {
    if (!blob) return;
    const objectURL = URL.createObjectURL(blob);
    setUrl(objectURL);

    return () => URL.revokeObjectURL(objectURL); // cleanup
  }, [blob]);

  return url;
}
Enter fullscreen mode Exit fullscreen mode

Large File Processing

For files over ~100MB, don't load the whole thing into memory at once.

// Process in chunks
async function processLargeFile(file, chunkSize = 1024 * 1024) { // 1MB chunks
  let offset = 0;

  while (offset < file.size) {
    const chunk = file.slice(offset, offset + chunkSize);
    const buffer = await chunk.arrayBuffer();

    await processChunk(buffer);
    offset += chunkSize;

    // Update progress
    const progress = Math.min(100, Math.round((offset / file.size) * 100));
    setProgress(progress);
  }
}
Enter fullscreen mode Exit fullscreen mode

File System Access API (Modern Browsers)

Chrome 86+ supports direct filesystem access:

// Pick a file
const [fileHandle] = await window.showOpenFilePicker({
  types: [{ 
    description: "Images", 
    accept: { "image/*": [".jpg", ".png", ".webp"] } 
  }],
});
const file = await fileHandle.getFile();

// Write back to the same file
const writable = await fileHandle.createWritable();
await writable.write(processedBlob);
await writable.close();
Enter fullscreen mode Exit fullscreen mode

This is powerful for editors — users can open, edit, and save without downloading a new file.

React Pattern for File Tools

function FileTool() {
  const [file, setFile] = useState(null);
  const [result, setResult] = useState(null);
  const inputRef = useRef(null);

  const onDrop = useCallback((e) => {
    e.preventDefault();
    const dropped = e.dataTransfer.files[0];
    if (dropped) setFile(dropped);
  }, []);

  const process = async () => {
    if (!file) return;
    const output = await processFile(file);
    setResult(output);
  };

  // Cleanup URLs on unmount
  useEffect(() => {
    return () => {
      if (result?.url) URL.revokeObjectURL(result.url);
    };
  }, [result]);

  return (
    <div
      onDrop={onDrop}
      onDragOver={(e) => e.preventDefault()}
    >
      {!file ? (
        <div onClick={() => inputRef.current?.click()}>
          Drop file here or click to select
          <input
            ref={inputRef}
            type="file"
            style={{ display: "none" }}
            onChange={(e) => setFile(e.target.files[0])}
          />
        </div>
      ) : (
        <>
          <div>{file.name} ({formatBytes(file.size)})</div>
          <button onClick={process}>Process</button>
        </>
      )}

      {result && (
        <a href={result.url} download={result.name}>
          Download
        </a>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

These patterns are used throughout ToolZip — 52 browser-based file processing tools.

Top comments (0)