ScreenshotNeo

BlogHow-to

How to Build an Image Preview in React

Build an accessible React image preview with file inputs, object URL cleanup, validation, troubleshooting, and a complete working component.

By the ScreenshotNeo team1 October 20268 min read

Use a file input, create a temporary object URL for the selected File, render the preview only while that URL exists, and revoke the URL when the file is replaced, cleared, or the component unmounts. A local preview does not upload the file; it only displays data already in the browser.

This guide builds the pattern in React, explains the object URL lifecycle, covers accessibility and validation boundaries, and includes fixes for common failures.

1. Complete React image preview component

The component below supports selecting one image, clearing it, replacing it, displaying a useful accessible description, and releasing object URLs at the right time.

import { useEffect, useState } from 'react';

export default function ImagePreview() {
  const [file, setFile] = useState(null);
  const [previewUrl, setPreviewUrl] = useState('');
  const [error, setError] = useState('');

  useEffect(() => {
    if (!file) {
      setPreviewUrl('');
      return;
    }

    const url = URL.createObjectURL(file);
    setPreviewUrl(url);

    // Runs when a new file replaces this one or when the component unmounts.
    return () => URL.revokeObjectURL(url);
  }, [file]);

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

    if (!nextFile) {
      setFile(null);
      return;
    }

    if (!nextFile.type.startsWith('image/')) {
      setFile(null);
      setError('Choose an image file.');
      event.target.value = '';
      return;
    }

    setFile(nextFile);
  }

  function clearSelection() {
    setFile(null);
    setError('');
    // Clearing the input lets the user choose the same file again.
    const input = document.getElementById('image-file');
    if (input) input.value = '';
  }

  return (
    <section>
      <h1>Image preview</h1>
      <label htmlFor="image-file">Choose an image</label>
      <input
        id="image-file"
        type="file"
        accept="image/*"
        onChange={handleChange}
      />

      {error && <p role="alert">{error}</p>}

      {previewUrl && file && (
        <figure>
          <img
            src={previewUrl}
            alt={`Selected image: ${file.name}`}
            style={{ maxWidth: '100%', height: 'auto' }}
          />
          <figcaption>{file.name}</figcaption>
          <button type="button" onClick={clearSelection}>
            Remove image
          </button>
        </figure>
      )}
    </section>
  );
}

React’s <input> reference documents type="file", accept, and change handling. The <img> reference covers src, alt, and why an empty-string source should be avoided.

2. How the file-to-preview flow works

  1. The user chooses a file with the browser’s file picker.
  2. event.target.files[0] returns a File object, or no file if the selection was cancelled.
  3. URL.createObjectURL(file) creates a temporary browser URL for that file.
  4. React stores the URL in state and passes it to <img src={previewUrl}>.
  5. When the selection changes or the component unmounts, the effect cleanup calls URL.revokeObjectURL(url).

Each call to createObjectURL creates a distinct URL. The MDN file API guide recommends releasing object URLs when they are no longer needed. Do not revoke the URL immediately in the image’s onLoad handler if users should still be able to open, save, or interact with the preview; revoke it when the preview is replaced, removed, or destroyed.

Why the cleanup belongs in useEffect

The effect runs whenever file changes. Its cleanup runs before the next effect and on unmount, so every URL is paired with one release operation. This prevents a growing collection of unused object URLs during repeated selections.

3. Accessibility and rendering rules

  • Give the file input a visible <label> connected with htmlFor and id.
  • Use meaningful alt text when the preview conveys information, such as the selected filename or a description supplied by the user.
  • Use alt="" when the image is purely decorative.
  • Render no image element until a usable URL exists. Do not render src="".
  • Expose validation errors with role="alert" or another announcement mechanism.
  • Keep the remove control keyboard accessible and use type="button" so it does not submit a surrounding form accidentally.

4. File type, size, and dimension checks

accept="image/*" filters the chooser in supporting browsers, but it is a hint rather than a security boundary. Validate the selected file in your application and validate it again on the server before storing or processing it.

function validateImage(file) {
  const maxBytes = 5 * 1024 * 1024;
  const allowedTypes = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);

  if (!allowedTypes.has(file.type)) return 'Use JPEG, PNG, WebP, or GIF.';
  if (file.size > maxBytes) return 'Choose an image smaller than 5 MB.';
  return '';
}

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

  const validationError = validateImage(nextFile);
  if (validationError) {
    setFile(null);
    setError(validationError);
    event.target.value = '';
    return;
  }

  setError('');
  setFile(nextFile);
}

For dimensions, wait until the object URL has loaded in an Image object, then inspect naturalWidth and naturalHeight. Treat MIME type, extension, dimensions, and file contents as separate checks. A preview does not prove that the file is safe, valid, or uploaded.

function readImageDimensions(file) {
  return new Promise((resolve, reject) => {
    const url = URL.createObjectURL(file);
    const image = new Image();
    image.onload = () => {
      const dimensions = {
        width: image.naturalWidth,
        height: image.naturalHeight,
      };
      URL.revokeObjectURL(url);
      resolve(dimensions);
    };
    image.onerror = () => {
      URL.revokeObjectURL(url);
      reject(new Error('The file could not be decoded as an image.'));
    };
    image.src = url;
  });
}

5. Previewing an image and uploading it are different steps

The object URL points to a local browser resource. It does not send bytes to your API, create a database record, or make the image available to another user. Upload only when the user submits the form.

async function uploadImage(file) {
  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 with status ${response.status}`);
  }

  return response.json();
}

Do not set the Content-Type header manually for this request; the browser adds the multipart boundary for FormData. Apply authentication, authorization, malware scanning, storage rules, and server-side validation according to your application.

6. Variations you may need

Multiple images

Use multiple, store an array of files, and create one object URL per file. Keep the URLs in state or derive them in an effect, then revoke every URL in cleanup.

Drag and drop

Read event.dataTransfer.files from onDrop and pass the first file through the same validation function used by the input. Keep the hidden input as a keyboard and mobile fallback.

Existing remote image plus a new local replacement

Keep the remote URL and local object URL as separate values. Revoke only the object URL. Do not call revokeObjectURL on an HTTPS URL from your server.

Resetting a form

When a parent form resets, clear both the React file state and the native input value. If the user chooses the same file twice, explicitly setting input.value = '' after removal ensures a new change event can fire.

7. Troubleshooting

Symptom Cause Fix
No preview appears The change handler reads the wrong field or state still contains an empty URL. Use event.target.files?.[0] ?? null and render only when previewUrl is truthy.
Preview breaks after loading The object URL was revoked in onLoad. Revoke it in effect cleanup, after removal, replacement, or unmount.
Choosing the same file does nothing The native input value did not change. Set the input value to an empty string when clearing or rejecting a file.
Wrong file type gets through accept is only a chooser hint. Check file.type, size, and decoded contents in the client and server.
Memory grows after many selections Object URLs are never released. Return URL.revokeObjectURL(url) from the effect and revoke temporary validation URLs.
Image is stretched CSS forces both dimensions. Set one dimension to auto; for example, max-width: 100%; height: auto.
Screen reader gives no useful description The image has missing or generic alternative text. Provide informative alt text, or use an empty string for decorative imagery.
Upload endpoint receives no file The preview URL was uploaded instead of the original File. Append the original File object to FormData.

8. Performance, reliability, and security notes

  • Object URLs avoid converting the whole file to a base64 string, which keeps preview setup simple and avoids enlarging the data representation.
  • Large images can still consume memory when decoded for display. Enforce a reasonable client-side size limit and consider server-side resizing after upload.
  • Use CSS constraints so a very large image does not overflow the layout.
  • Handle decode failures with an error state; a filename ending in .jpg is not proof that the bytes are a valid JPEG.
  • Never rely on client-side checks alone for security. Revalidate uploads on the server, restrict storage and response headers, and apply your content policy.
  • Keep the original File in state only as long as needed for submission. Clear it after a successful upload if the workflow is complete.

9. Or skip the browser setup

If your requirement is to capture a preview of a web page, a hosted screenshot is simpler than maintaining browser automation. ScreenshotNeo returns PNG, JPEG, WebP, or PDF from one GET request. Cookie banners, newsletter popups, and chat widgets are removed before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Its response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.

See the ScreenshotNeo API documentation for all options. Basic cURL:

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

Python:

import requests

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

Node.js:

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', bytes);

ScreenshotNeo includes full-page capture, lazy-image loading, element selectors, custom CSS and JavaScript, device presets, dark mode, PDF settings, waits, request blocking, headers, cookies, geolocation, caching, signed links, async jobs, bulk capture, usage data, and an OpenAPI specification. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

10. FAQ

Does a preview upload the image?

No. URL.createObjectURL creates a local browser reference. Upload the original File separately when the user submits.

Can I revoke the URL immediately after onLoad?

Only if you no longer need the image. For a usable preview that users can open or save, revoke it during replacement, removal, or component unmount instead.

Is accept="image/*" secure validation?

No. It influences the chooser. Validate type, size, contents, and authorization on the server as well.

Why should the image be conditional?

React advises against an empty-string src. Conditional rendering ensures the image receives a real URL.

Should decorative previews have alt text?

Use alt="" when the image adds no information. Otherwise describe what the user needs to understand.