React useObjectUrl Hook: Preview Files & Blobs Without Memory Leaks (2026)
A user uploads a dozen images to a gallery editor. Each one gets a live preview via URL.createObjectURL(). The session runs for twenty minutes, previews get swapped as files are added and removed, and the tab's memory usage climbs the entire time — because every createObjectURL() call has to be paired with a revokeObjectURL() call, and that pairing has to survive re-renders, prop changes, and unmounts. Most components get it wrong, and the bug is invisible until someone leaves the tab open long enough to notice the fan spin up.
useObjectUrl is the one-line fix: hand it a File, Blob, or MediaSource, get back a URL string, and never call revokeObjectURL yourself again. Everything below is the real @reactuses/core API, TypeScript-first.
The Manual Version, and Where It Leaks
URL.createObjectURL() is the browser API behind every client-side file preview: pass it a Blob (a File is one) and it hands back a blob: URL you can drop straight into an <img src> or <video src>. The catch is on the other end — every URL it mints stays alive, holding a reference to the underlying data, until you explicitly call URL.revokeObjectURL(). Forget that call, or call it at the wrong time, and the URL — and the memory behind it — never gets released.
In a React component, "the wrong time" is easy to hit:
function FilePreview({ file }: { file?: File }) {
const [url, setUrl] = useState<string>();
useEffect(() => {
if (!file) return;
const next = URL.createObjectURL(file);
setUrl(next);
return () => URL.revokeObjectURL(next);
}, [file]);
return url ? <img src={url} /> : null;
}
This one is actually correct — the cleanup closes over next, not over state, so it always revokes exactly the URL it created. But it's correct in the way hand-rolled effects tend to be: one careless refactor away from breaking. Move the URL.createObjectURL call outside the effect, memoize it wrong, or read url from state instead of the local next, and the pairing silently comes apart. Multiply this pattern across every component that previews a file — an avatar uploader, a chat attachment, a gallery thumbnail — and you're maintaining the same fragile lifecycle logic in five different places.
useObjectUrl — Create and Revoke, Handled
import { useObjectUrl } from '@reactuses/core';
function FilePreview({ file }: { file?: File }) {
const url = useObjectUrl(file);
return url ? <img src={url} /> : null;
}
That's the whole hook. The signature:
function useObjectUrl(object: Blob | MediaSource): string | undefined;
Pass a Blob (or the File subtype of it) or a MediaSource, get back the object URL — or undefined before a source exists. Internally it's one useEffect: call URL.createObjectURL() when the source is present, and return a cleanup that calls URL.revokeObjectURL(). The difference from the hand-rolled version isn't behavior, it's that the pairing lives in one audited place instead of being re-derived per component.
When the Source Changes — and When It Unmounts
The revoke fires in both cases that matter:
-
The source changes. Swap the
fileprop for a differentFileand the hook revokes the old URL before minting the new one — no accumulation as a user clicks through a list of attachments. - The component unmounts. Close the preview modal, navigate away, remove the item from a list — the cleanup runs and the URL is released. Nothing is left waiting for a garbage collector that doesn't know to collect it.
That second case is the one hand-rolled code most often skips, because it only shows up as a slow memory creep during actual use, not in a quick manual test.
Real Use Cases
- File input previews. Show the image, video, or PDF a user just selected before they commit to uploading it — the canonical case.
-
Blob API responses. An endpoint that returns a
Blob(a generated export, a signed asset, afetch().blob()result) can be turned into a downloadable<a href>or an inline<img>/<video>without writing it to disk. -
Canvas exports.
canvas.toBlob()produces aBlob; feed it straight intouseObjectUrlto preview or offer a download of what the user just drew or cropped. -
MediaSourcefor custom video players.MediaSourceis the Media Source Extensions object behind adaptive streaming and custom-buffered video — you build the stream programmatically, then still need a URL to hand to<video src>.useObjectUrlaccepts it directly, same as aBlob.
SSR-Safe by Construction
URL.createObjectURL doesn't exist on the server — there's no DOM, no Blob URLs to mint. useObjectUrl never touches the URL API during server rendering; it returns undefined until the effect runs on the client, so there's no typeof window guard to remember and no server crash to debug. If you're auditing other hand-rolled browser-API code for the same gap, SSR-Safe React Hooks covers the general pattern.
The File-Handling Trio
useObjectUrl is one piece of a small set of hooks that cover the full file lifecycle — picking a file, dropping a file, and previewing it:
| Hook | Role |
|---|---|
useFileDialog |
Opens the native file picker, returns the selected FileList
|
useDropZone |
Turns any element into a drag-and-drop target for files |
useObjectUrl |
Turns the resulting File/Blob into a safe, auto-revoked preview URL |
Wire a useFileDialog or useDropZone result straight into useObjectUrl and the preview step needs no cleanup code at all. The full walkthrough — including a multi-file gallery that combines all three — is in React File Handling.
Takeaways
-
The core problem is a pairing bug. Every
URL.createObjectURL()needs a matchingURL.revokeObjectURL(), and re-deriving that pairing correctly in every component that shows a file preview is where memory leaks creep in. -
useObjectUrl(object)takes aBlob(orFile) orMediaSourceand returns the URL string, orundefinedbefore a source exists. - Revocation is automatic on both triggers that matter: the source changing and the component unmounting.
- Covers more than image previews: Blob API responses,
canvas.toBlob()exports, andMediaSourcefor custom video players. -
SSR-safe — returns
undefinedon the server, noURLAPI access, no guard code needed. - Pairs with
useFileDialoganduseDropZoneto cover selection, drop, and preview with zero manual lifecycle management.
Grab it from @reactuses/core and stop auditing components for missing revokeObjectURL calls.
Top comments (0)