React Development

Solved: React Pics Fail Locally, Work on Deploy (2025)

Struggling with React images that work on deploy but not locally? This 2025 guide solves the common pathing issue between `public` and `src` folders. Fix it now.

D

Daniel Carter

Senior Frontend Engineer specializing in React, performance optimization, and modern web architecture.

7 min read6 views

The Frustrating Paradox: Why Do Images Break Locally?

It’s a scenario that has stumped countless React developers: you meticulously place your images, style your components, and everything looks pixel-perfect on your `localhost:3000`. You commit your code, push it, and the deployment pipeline runs successfully. You open the live URL from Vercel or Netlify, beaming with pride, only to be greeted by broken image icons. Your beautiful visuals work perfectly on your machine but are completely gone on the deployed version. Or, sometimes, the reverse happens: they fail locally but magically appear on deploy.

If you're facing this maddening issue in 2025, you're in the right place. This isn't a bug in React or a problem with your hosting provider. It's a fundamental misunderstanding of how modern JavaScript build tools like Vite and Create React App (CRA) handle static assets. This guide will solve this problem for you once and for all by explaining the 'why' and providing clear, actionable solutions.

The Core Problem: Dev Server vs. Production Build

The entire issue boils down to one key difference: the environment your code runs in during development versus in production.

  • Development Server (`localhost`): When you run `npm run dev` or `npm start`, a local development server is created. This server is optimized for speed and hot-reloading. It serves your `public` folder as the root directory (`/`). If you have an image at `public/images/logo.png`, your browser can access it directly at `http://localhost:3000/images/logo.png`. It's simple and direct.
  • Production Build (`npm run build`): When you deploy your app, your hosting provider runs a build command. This process, managed by a bundler like Vite or Webpack, is optimized for performance and size. It takes all your code and assets (JavaScript, CSS, images in your `src` folder), compresses them, minifies them, and—crucially—renames them with a unique hash (e.g., `logo.a8b4c2d1.png`). This is done for cache-busting, ensuring users get the latest version of your assets. The bundler then automatically updates the paths in your code to point to these new, hashed filenames.

The conflict arises when you use a path that works for one environment but not the other. An incorrect path might accidentally work on the dev server but will fail during the build process because the bundler doesn't know to include and process that asset.

Solution 1: The `public` Folder Method (The Quick Fix)

Most React frameworks (including Vite and Create React App) come with a `public` directory. This folder is a special escape hatch from the build process.

How It Works

Any file you place in the `public` folder is not processed by the bundler. Instead, it's copied directly into the root of your final build directory (`dist` or `build`).

Let's say your project structure is:

my-react-app/
├── public/
│   └── images/
│       └── hero-banner.jpg
└── src/
    └── components/
        └── Hero.jsx

To display `hero-banner.jpg` from your `Hero` component, you would reference it with an absolute path from the root:

function Hero() {
  return (
    <div>
      <h1>Welcome to Our Site</h1>
      {/* The path starts with '/' to refer to the public folder root */}
      <img src="/images/hero-banner.jpg" alt="A stunning hero banner" />
    </div>
  );
}

export default Hero;

Why this works both locally and on deploy: The dev server serves `public` as the root, so `/images/hero-banner.jpg` resolves correctly. In the production build, the `images` folder is copied to the root of the output directory, so the same absolute path continues to work on your live domain.

When to Use It

The `public` folder is best for assets that:

  • You need to reference by a specific, unchanged name (e.g., `robots.txt`).
  • Are outside the module system, like a third-party script.
  • You don't want processed by the bundler for any reason.

Warning: Using this method for all your images means you lose out on build-time optimizations, hashing for cache-busting, and compile-time checks (if you misspell the filename, you won't get an error until you see the broken image in your browser).

Solution 2: The `import` Method (The Modern Standard)

The recommended approach for component-specific images is to keep them inside your `src` folder and `import` them directly into your component file.

How It Works

When you import an image, you're telling the bundler, "I need this asset." The bundler will then include the image in the build process, optimize it, give it a unique hashed filename, and the `import` statement will return the final, correct public URL as a string.

Let's adjust our project structure:

my-react-app/
├── public/
└── src/
    └── assets/
    │   └── images/
    │       └── logo.svg
    └── components/
        └── Header.jsx

In your `Header` component, you import the image and use the resulting variable in your `src` attribute:

import React from 'react';
import logoImage from '../assets/images/logo.svg'; // The path is relative to this file

function Header() {
  return (
    <header>
      {/* The 'logoImage' variable holds the final public path to the image */}
      <img src={logoImage} alt="Our company logo" />
    </header>
  );
}

export default Header;

Why this is robust: You are no longer hardcoding a path. The bundler manages the path for you. If the image is moved or renamed in the final build, `logoImage` will always contain the correct URL. Furthermore, if you misspell the path in your `import` statement (`'../assets/imagess/logo.svg'`), your application will fail to compile, alerting you to the error immediately—a huge advantage.

When to Use It

This is the best practice for almost all images used within your React components, such as logos, icons, user avatars, and banners.

Comparison: `public` Folder vs. `import` Method

Asset Handling Method Comparison
Feature`public` Folder Method`import` Method (from `src`)
PathingAbsolute path from the domain root (e.g., `src="/images/pic.png"`).Import the asset and use the variable (e.g., `src={myPic}`).
Build ProcessFile is copied as-is. No optimization or hashing.File is processed, optimized, and renamed with a hash.
Error HandlingNo build-time error for missing files. Results in a 404 in the browser.Build fails if the image path is incorrect, catching errors early.
CachingRelies on standard browser/server caching. Can be problematic.Hashed filenames automatically break browser cache on updates.
Best ForStatic assets like `favicon.ico`, `robots.txt`, or manifest files.Component-specific images, logos, icons, and any asset that benefits from optimization.

Troubleshooting Common Edge Cases

Even with these two methods, some situations can be tricky. Here’s how to handle them.

Dynamic Image Paths

What if you need to load an image based on props or state? The `import` method fails here because the bundler needs to know the exact file path at build time.

// This will NOT work with the import method
function UserAvatar({ imageName }) {
  // The bundler cannot resolve this dynamic string
  const imagePath = `../assets/images/${imageName}`;
  return <img src={imagePath} alt="User avatar" />;
}

Solution: This is a prime use case for the `public` folder. Place your dynamic images (e.g., a collection of user avatars) in `public/avatars/`. Then, you can construct the path string dynamically.

// This WILL work
function UserAvatar({ imageName }) {
  const imagePath = `/avatars/${imageName}`; // e.g., /avatars/user-1.png
  return <img src={imagePath} alt="User avatar" />;
}

Vite-specific solution: If you're using Vite, you can use a special `URL` constructor to handle dynamic assets that are still in your `src` folder. This gives you the best of both worlds.

// Vite-only dynamic import
function getImageUrl(name) {
  return new URL(`../assets/images/${name}`, import.meta.url).href
}

function UserAvatar({ imageName }) {
  const imagePath = getImageUrl(imageName);
  return <img src={imagePath} alt="User avatar" />;
}

Case Sensitivity: Linux vs. macOS/Windows

A classic pitfall. Your code might reference `logo.PNG`, but the file is named `logo.png`. On your case-insensitive file system (macOS or Windows), this works fine locally. However, most deployment environments run on Linux, which has a case-sensitive file system. The build will fail, or you'll get a 404 on the live site. Always ensure your file names and the references in your code match exactly.

Background Images in CSS

When using `background-image` in a CSS file that is imported into your React app, the bundler is smart enough to handle it correctly. You can use a relative path from the CSS file.

/* src/components/Hero.css */
.hero-container {
  background-image: url('../assets/images/hero-bg.jpg');
}

The bundler will process `hero-bg.jpg` just like an imported image and replace the `url()` with the correct final path.