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
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
As ArrayBuffer
const buffer = await file.arrayBuffer();
// Use for: binary files, passing to WASM libraries
// ArrayBuffer is a fixed-length binary buffer
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
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);
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);
});
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;
}
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;
}
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);
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);
}
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;
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;
}
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);
}
}
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();
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>
);
}
These patterns are used throughout ToolZip — 52 browser-based file processing tools.
Top comments (0)