Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use a file input, read the first selected File, create a temporary URL with URL.createObjectURL(file), and render that URL in an <img>. This displays the image locally; it does not upload, validate, or store it. Keep the original File for your later upload and revoke each object URL when its preview is replaced or removed.

Complete React image-preview component

The component below supports one image, handles cancellation, clears an existing selection, releases object URLs, and keeps the original File available for submission. It uses a ref for the active URL so replacement cleanup happens immediately and unmount cleanup does not depend on a stale state value.

import { useEffect, useRef, useState } from 'react';

export default function ImagePicker() {
  const [file, setFile] = useState(null);
  const [previewUrl, setPreviewUrl] = useState(null);
  const inputRef = useRef(null);
  const objectUrlRef = useRef(null);

  function releasePreview() {
    if (objectUrlRef.current) {
      URL.revokeObjectURL(objectUrlRef.current);
      objectUrlRef.current = null;
    }
  }

  function handleChange(event) {
    const nextFile = event.target.files?.[0] ?? null;

    releasePreview();
    setFile(nextFile);

    if (nextFile) {
      const nextUrl = URL.createObjectURL(nextFile);
      objectUrlRef.current = nextUrl;
      setPreviewUrl(nextUrl);
    } else {
      setPreviewUrl(null);
    }
  }

  function clearSelection() {
    releasePreview();
    setFile(null);
    setPreviewUrl(null);
    // Allows selecting the same file again after clearing it.
    if (inputRef.current) inputRef.current.value = '';
  }

  useEffect(() => {
    return () => releasePreview();
  }, []);

  async function handleSubmit(event) {
    event.preventDefault();
    if (!file) return;

    const formData = new FormData();
    formData.append('image', file);

    const response = await fetch('/api/images', {
      method: 'POST',
      body: formData
    });

    if (!response.ok) {
      throw new Error(`Upload failed: ${response.status}`);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="image-file">Choose an image</label>
      <input
        ref={inputRef}
        id="image-file"
        type="file"
        accept="image/*"
        onChange={handleChange}
      />

      {previewUrl && (
        <figure>
          <img
            src={previewUrl}
            alt={file ? `Preview of ${file.name}` : 'Preview of selected image'}
            style={{ maxWidth: '100%', height: 'auto' }}
          />
          <figcaption>{file?.name}</figcaption>
        </figure>
      )}

      <button type="button" onClick={clearSelection} disabled={!file}>
        Remove image
      </button>
      <button type="submit" disabled={!file}>Upload image</button>
    </form>
  );
}

accept="image/*" guides the picker toward image files, but it is not a security boundary. A user can provide a file with misleading metadata, so enforce your size and content rules in the browser for useful feedback and repeat authoritative validation on the server.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How the preview flow works

  1. The input emits a change event. React passes the event to handleChange; the selected files are in event.target.files.
  2. The handler chooses a file. The example takes the first entry and deliberately handles the no-file case, which occurs when the picker is cancelled or the control is cleared.
  3. The browser creates a local object URL. URL.createObjectURL(file) returns a temporary URL that an image element can display without sending the file to your server.
  4. React renders only when a URL exists. Conditional rendering avoids an empty src, which can cause an image request you did not intend.
  5. The original file remains the upload payload. Submit file with FormData; do not send the object URL as if it were the image itself.

Previewing is therefore a client-side display step. It does not prove that the image is safe, accepted by your API, uploaded successfully, or stored permanently.

Releasing object URLs correctly

Every call to URL.createObjectURL creates a distinct URL. Release the previous one before replacing a preview, release it when the user removes the image, and release it when the component unmounts. The example’s releasePreview function centralizes those paths.

Do not revoke the URL immediately after the image’s load event if the user still needs to view, zoom, interact with, or otherwise use the preview. Keep it alive for the preview’s lifetime, then revoke it during replacement, removal, or unmount. Avoid creating object URLs during render: React can render repeatedly, and render-time side effects can leave URLs without a matching cleanup.

Validation before the upload

Check presence and size

const MAX_BYTES = 5 * 1024 * 1024;

function validateImage(file) {
  if (!file) return 'Choose an image first.';
  if (!file.type.startsWith('image/')) return 'Select an image file.';
  if (file.size > MAX_BYTES) return 'The image must be 5 MB or smaller.';
  return null;
}

Call this function before enabling submission or before constructing FormData. Treat the browser check as user feedback, not as the final trust decision; inspect the actual content and enforce limits again on the server.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Check dimensions when your product needs them

If a profile photo or banner has minimum dimensions, load the preview in an Image object and inspect its natural width and height before upload. Keep the object URL until that check and the user’s preview are finished, then release it through the same cleanup path. Do not claim that a file’s extension or MIME string alone establishes its dimensions or safety.

Use useful alternative text

Give the preview an alt that identifies what it represents, such as the selected filename or “Preview of selected image.” If the image is purely decorative and a nearby text label already conveys its purpose, use an empty alt instead. Keep the file input’s visible label associated with it through htmlFor and id.

Uploading the selected File

The preview and upload are separate actions. A typical submit handler sends the original file as multipart form data:

async function uploadImage(file) {
  const error = validateImage(file);
  if (error) throw new Error(error);

  const body = new FormData();
  body.append('image', file, file.name);

  const response = await fetch('/api/images', {
    method: 'POST',
    body
  });

  if (!response.ok) {
    throw new Error(`Upload failed with HTTP ${response.status}`);
  }

  return response.json();
}

When using FormData, let the browser set the multipart boundary; do not manually set a Content-Type header unless your server integration specifically requires a correctly generated boundary. Add progress, retry, authentication, and server response handling according to your API rather than treating a rendered preview as evidence that the request succeeded.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Object URL versus FileReader data URL

FileReader.readAsDataURL(file) is the other browser-native approach. It asynchronously reads the File or Blob and exposes a data: URL when reading completes.

import { useEffect, useState } from 'react';

function DataUrlPreview({ file }) {
  const [dataUrl, setDataUrl] = useState(null);

  useEffect(() => {
    if (!file) {
      setDataUrl(null);
      return;
    }

    const reader = new FileReader();
    reader.onload = () => setDataUrl(typeof reader.result === 'string' ? reader.result : null);
    reader.onerror = () => setDataUrl(null);
    reader.readAsDataURL(file);

    return () => {
      reader.onload = null;
      reader.onerror = null;
      if (reader.readyState === FileReader.LOADING) reader.abort();
    };
  }, [file]);

  return dataUrl ? <img src={dataUrl} alt="Preview of selected image" /> : null;
}
Aspect Object URL Data URL with FileReader
Preview source A temporary browser URL that references the File or Blob Encoded file contents in a data: URL
Lifecycle Explicitly revoke the URL when its preview use ends Wait for the asynchronous read; retain the resulting string only as needed
Best fit A temporary local display When another part of the application specifically needs a data URL

Choose based on the representation and lifecycle your application needs. There is no measured performance conclusion here that justifies treating one as universally faster.

Multiple-image previews

For a gallery, add multiple, map every selected file to a preview record, and keep each URL paired with its file:

const nextItems = Array.from(event.target.files ?? []).map((file) => ({
  file,
  url: URL.createObjectURL(file)
}));
setItems(nextItems);

Before replacing the array, iterate over the old records and call URL.revokeObjectURL(item.url). When one item is deleted, revoke only that item’s URL. On unmount, revoke every remaining URL. Do not keep a single shared URL ref for multiple files.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Common problems and fixes

The preview is blank

  • Render the img only when the URL is non-null; never pass an empty string as src.
  • Confirm that the handler reads event.target.files?.[0], not a nonexistent event.target.file.
  • Inspect the browser console for an image decoding error and verify the selected file is actually an image.

Choosing Cancel removes the old preview unexpectedly

Decide your product behavior explicitly. The example treats a no-file change as a deliberate clear. If cancellation should preserve the existing image, return early when files?.[0] is absent and provide a separate Remove button for clearing.

The same file cannot be selected twice

After removing a file, set the input’s value to an empty string, as clearSelection does. This lets a later selection of the same path produce a new change event.

Memory usage grows after repeated replacements

Revoke the prior URL before creating its replacement and revoke all URLs on unmount. Do not create URLs in render or in an effect that runs on every render without a matching cleanup.

The upload succeeds but the server rejects the image

That is expected when server rules differ from client hints. Return a clear API error, show it near the input, and validate content, dimensions, and size on the server. A local preview does not bypass those checks.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The image disappears after immediate revocation

Do not revoke in the image’s onLoad handler while the user still needs the displayed image. Revoke when the preview is replaced, removed, or the component unmounts.

Performance and reliability choices

  • Keep only the state you need: store the original File and its current preview URL, not duplicate encoded data unless your application needs it.
  • Prevent oversized work: reject clearly excessive files before reading or uploading them, while still enforcing the rule on the server.
  • Handle races: if a user selects files quickly, cancel an in-progress FileReader and ignore stale asynchronous callbacks.
  • Preserve user control: do not revoke a still-visible preview merely because it finished loading.
  • Separate states: represent “no selection,” “preview ready,” “uploading,” “uploaded,” and “upload failed” independently so a failed request does not erase a usable local preview.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If what you actually need is an automated screenshot of a rendered page or preview workflow—not a local file preview inside your React form—ScreenshotNeo provides a website screenshot API and MCP server. It accepts one GET request and returns PNG, JPEG, WebP, or PDF. Before capture it can accept cookie banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP tools—take_screenshot, get_page_info, and capture_pdf—work with Claude, Cursor, and other MCP clients.

cURL (see the ScreenshotNeo API documentation):

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/image-preview -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/image-preview"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/image-preview' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Every feature is available on every plan: full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, PDF controls, HTML/CSS rendering, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API, an OpenAPI specification, and familiar parameter names for easier switching.

Plan Allowance and price
Free 1,000 shots per month, no card
Starter $5 for 3,000 shots
Growth $15 for 15,000 shots
Pro $39 for 60,000 shots
Scale $99 for 250,000 shots
Business $249 for 1,000,000 shots

Yearly billing gives two months free. Start with 1,000 free ScreenshotNeo screenshots a month with no card, or use a paid plan starting at $5 for 3,000 shots.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Final checklist

  • Use accept="image/*" as a picker hint, not as validation.
  • Read the selected File from the input’s files collection.
  • Create the preview URL in an event handler or controlled effect, never during render.
  • Render the image only when the source is nonempty.
  • Keep the original File for FormData upload.
  • Revoke each object URL after replacement, removal, or unmount, but not while the user still needs the preview.
  • Validate again on the server.

Frequently Asked Questions

Can I submit the object URL to my upload endpoint?

No. The object URL is only a browser reference for rendering. Submit the original File, normally in FormData.

Do I need FileReader for a normal React preview?

No. URL.createObjectURL is sufficient for a temporary display. Use FileReader when another part of the application specifically requires a data URL.

What should happen when a user cancels the file picker?

Choose deliberately between preserving the current selection and clearing it. Handle the no-file case explicitly rather than assuming a File is always present.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.