React

7 Secrets to Fix React Local Images That Work on Deploy

Tired of React images breaking on deploy? Uncover 7 secrets to handle local images correctly, from the public folder to dynamic imports with Vite. Fix it for good.

A

Alex Porter

Senior Frontend Engineer specializing in React, performance optimization, and build tooling.

8 min read13 views

The “It Works On My Machine” Image Problem

Every React developer has faced it. You meticulously place your images in the `src/assets` folder, reference them in your components with a simple ``, and everything looks perfect on your local development server. Then comes deployment. You push your code, the build process runs, and... your beautiful images are replaced by ugly, broken icons. It’s a frustrating rite of passage, but one that is entirely fixable once you understand what's happening behind the scenes.

This comprehensive guide will demystify the process and unveil the seven essential secrets to handling local images in React. By the end, you'll be able to manage your assets with confidence, ensuring they appear flawlessly from your local machine to the live production server.

Why Do React Images Break on Deploy?

The root of the problem lies in the build process. When you run `npm run build`, tools like Webpack (used by Create React App) or Vite bundle your application for production. This isn't just a simple copy-paste of your files. The build process performs several critical optimizations:

  • Bundling: JavaScript and CSS files are combined and minified to reduce the number of network requests.
  • Transpilation: Modern JavaScript (ES6+) is converted to an older version (ES5) for wider browser compatibility.
  • Asset Hashing: This is the key culprit for broken images. To enable effective browser caching, build tools rename your assets (like `logo.png`) to include a unique hash (`logo.a8d5f6.png`). If you hardcode the path ``, the browser will look for that exact file, which no longer exists in the final `build` or `dist` directory.

The secret, therefore, is not to fight the build process but to work with it. You need to let your bundler know about your images so it can manage their paths for you. Let's explore how.

7 Secrets to Reliable React Images

Secret 1: The `public` Folder for Unprocessed Assets

Most React frameworks (including Create React App and Vite) come with a `public` folder. This folder is a special escape hatch from the build process. Any files you place inside it are not processed or hashed; they are copied directly into the root of your build directory.

How to use it:

  1. Place your image (e.g., `favicon.ico`) inside the `public` folder.
  2. Reference it with an absolute path from the root.
<!-- Assuming `logo.png` is in the `public` folder -->
<img src="/logo.png" alt="My Company Logo" />

Pros:

  • Extremely simple for static assets.
  • Perfect for files that must have a specific name and location, like `robots.txt` or manifest files.

Cons:

  • No cache busting: Since the filename never changes, browsers might serve an outdated version if you update the image.
  • No build-time checks: If you misspell the path, you won't get an error during the build; it will only fail silently in the browser.
  • Generally considered an anti-pattern for component-specific images.

Secret 2: `import` - The Gold Standard for Static Images

The recommended way to handle images that are part of your component's source code is to `import` them directly into your JavaScript file. This explicitly tells the build tool, "Hey, I'm using this asset!"

How to use it:

import React from 'react';
import myImage from './assets/beautiful-scenery.jpg'; // Path is relative to the JS file

function MyComponent() {
  return (
    <div>
      <h2>A Beautiful Scene</h2>
      <img src={myImage} alt="A beautiful mountain landscape" />
    </div>
  );
}

export default MyComponent;

When you do this, Webpack/Vite processes `beautiful-scenery.jpg`, moves it to the build folder with a hashed name (e.g., `beautiful-scenery.c3f7b2.jpg`), and the `myImage` variable will contain the correct public URL string to that final file.

Pros:

  • Automatic cache busting: The hash changes whenever the image content changes.
  • Build-time validation: The build will fail if the image is missing or the path is incorrect, catching errors early.
  • Co-location: You can keep images in the same folder as the components that use them, improving organization.

Cons:

  • Only works for static, known-at-build-time paths. It doesn't work if the image name is determined by props or state.

Secret 3: The `require()` Hack for Legacy Dynamic Paths

Before `import` became the standard, `require()` was common. While less used now, it can be a useful trick in older Create React App projects for handling semi-dynamic paths, as Webpack can sometimes resolve the context.

// This might work in a CRA environment
function UserAvatar({ imageName }) {
  // imageName could be 'user-1.png', 'user-2.png', etc.
  const imagePath = require(`./assets/avatars/${imageName}`).default;
  return <img src={imagePath} alt="User avatar" />;
}

Pros:

  • Can handle some dynamic path scenarios in specific Webpack configurations.

Cons:

  • Not a modern standard; it's a CommonJS feature.
  • Does not work out-of-the-box with Vite or modern ES Modules-first setups.
  • Can be unpredictable and is generally not recommended for new projects.

Secret 4: Vite's `new URL()` for Modern Dynamic Images

Vite, the modern build tool, provides a clean and explicit way to handle dynamic asset URLs. This is the definitive solution if you're working in a Vite environment and need to load an image based on a variable.

How to use it:

The pattern uses the `new URL(path, base)` constructor, where the base is `import.meta.url`. This tells Vite to resolve the asset path relative to the current module.

// In a Vite project
function getImageUrl(name) {
  // `name` could be 'icon-success' or 'icon-error'
  return new URL(`./assets/icons/${name}.svg`, import.meta.url).href;
}

function StatusIcon({ type }) {
  const iconUrl = getImageUrl(`icon-${type}`); // e.g., 'icon-success'
  return <img src={iconUrl} alt={`${type} status icon`} />;
}

Pros:

  • The standard, documented way to handle dynamic assets in Vite.
  • Explicit and clear about its intent.
  • Assets are still processed and hashed by the build tool.

Cons:

  • The syntax is specific to environments that support `import.meta.url` (i.e., Vite and modern browsers).

Secret 5: CSS `background-image` with `url()`

Don't forget that you can also manage images through CSS. When you reference an image in a CSS or SCSS file, the build process will parse it, find the asset, and replace the path with the final hashed URL, just like with a JavaScript `import`.

How to use it:

/* In MyComponent.css */
.hero-section {
  background-image: url('../assets/hero-background.jpg');
  background-size: cover;
  height: 400px;
}

// In MyComponent.js
import './MyComponent.css';

function MyComponent() {
  return <div className="hero-section">Welcome!</div>;
}

Pros:

  • Keeps presentational images separate from content.
  • Benefits from the same processing and hashing as JS imports.

Cons:

  • Not semantically correct for content images (`` tags are better for SEO and accessibility).
  • Harder to manipulate dynamically from component logic.

Secret 6: Offloading to a CDN for Scalability

For large-scale applications, you might not want to bundle all your images with your app. Hosting them on an external service like Amazon S3, Cloudinary, or another Content Delivery Network (CDN) is a powerful strategy.

You simply use the full, absolute URL provided by the service.

const userAvatarUrl = 'https://my-cdn.com/images/avatars/user-123.jpg';

function UserProfile() {
  return <img src={userAvatarUrl} alt="User Profile" />;
}

Pros:

  • Reduces your application's bundle size.
  • Faster load times for users via geographically distributed servers (CDN).
  • Allows for on-the-fly image transformations (resizing, cropping, filters).

Cons:

  • Adds another service to manage and potentially pay for.
  • You are responsible for your own asset management and versioning.

Secret 7: Mastering `process.env.PUBLIC_URL` in CRA

This is a specific trick for Create React App. `process.env.PUBLIC_URL` is an environment variable that resolves to the URL of your `public` folder. It's useful when your app is deployed to a subdirectory (e.g., `https://example.com/my-app/`).

Using `` would incorrectly point to `https://example.com/logo.png`. Using the environment variable ensures the path is always correct.

// In a Create React App project
<img src={`${process.env.PUBLIC_URL}/logo192.png`} alt="App Logo" />

Pros:

  • Makes references to the `public` folder robust and deployment-agnostic.

Cons:

  • Only works in Create React App.
  • Shares all the cons of using the `public` folder (no hashing, no build-time checks).

Comparison: `import` vs. `public` vs. `new URL()`

Choosing Your Image Handling Method
MethodBest ForProsCons
`import` StatementStatic images within your `src` folder (component logos, icons).Cache busting, build-time error checking, co-location with components.Does not work for dynamic paths determined at runtime.
`public` FolderAssets that must not be processed (e.g., `robots.txt`, favicons).Simple, direct file access with a predictable URL.No hashing (bad for caching), no build-time validation, generally an anti-pattern.
`new URL()` (Vite)Dynamic images in a Vite project where the path is from a variable.The modern standard for dynamic assets, benefits from build processing.Vite-specific syntax, slightly more verbose.

Conclusion: Choosing the Right Method

Fixing broken React images is about understanding and embracing the build process. Instead of fighting it with hardcoded paths, you can leverage its power to your advantage. By mastering these seven secrets, you can build more robust, performant, and error-free applications.

Moving forward, make the `import` statement your default choice for static images. If you are using Vite and need dynamic images, `new URL(..., import.meta.url)` is your best friend. Reserve the `public` folder for specific use cases where bypassing the build process is a requirement, not a shortcut.