Instantly Fix React Local Images: The 2025 Dev Guide
Tired of broken images in React? Our 2025 guide instantly fixes local image issues. Learn the public vs. src methods, handle dynamic images in Vite & CRA, and master best practices.
Alexei Petrov
Senior Frontend Engineer specializing in React performance and modern tooling.
The Frustration of the Broken Image Icon
We've all been there. You're building a beautiful React component, you add an `` tag with what you *know* is the correct path to your local image, and you're greeted by that infamous, tiny broken image icon. It’s a rite of passage for every React developer, but it's a frustrating one. You check the path a dozen times, move the file, and question your sanity. The good news? It's not you, it's the build process. And it's easily fixable.
This 2025 guide will demystify local image handling in modern React applications, whether you're using Create React App (CRA) or the increasingly popular Vite. We'll cover the fundamental concepts, the recommended best practices, and the modern patterns you need to know to make broken images a thing of the past.
Why Do React Images Break? The Core Concept
The root of the problem lies in understanding how modern JavaScript bundlers like Webpack (used by CRA) and Rollup (used by Vite) work. When you run `npm run build`, your entire `src` directory is processed, optimized, and bundled into a few static files that are placed in a `build` or `dist` folder. Your code is transformed, and so are the paths to your assets.
There are two primary locations for your static assets, and they are treated very differently by the bundler:
- The `public` Folder: This is an escape hatch. Anything in this folder is not processed by the bundler. It's copied directly into the root of the final `build` folder.
- The `src` Folder: This is where your React components, CSS, and—yes—your images should generally live. Anything in this folder is processed by the bundler. It's treated as a module.
Trying to use a relative path to an image in `src` like `` will fail because by the time that HTML is rendered in the browser, the file structure has completely changed. The browser is looking for `logo.png` relative to the final HTML file, but the bundler hasn't moved it there or provided the correct, post-build path.
Method 1: The `public` Folder - The Simple Path
The simplest way to display an image is to place it in the `public` folder. This method bypasses the bundler entirely.
How It Works
If you have an image at `public/images/my-logo.png`, the bundler will copy it to `build/images/my-logo.png`. Because it's served from the root of your application, you can reference it with an absolute path from the root.
In your JSX, you would write:
<img src="/images/my-logo.png" alt="My company logo" />
Notice the leading slash `/`. This tells the browser to look for the `images` folder from the domain's root, not relative to the current component. You can also use the `process.env.PUBLIC_URL` environment variable to be more explicit:
<img src={`${process.env.PUBLIC_URL}/images/my-logo.png`} alt="My company logo" />
When to Use It
Use the `public` folder for assets that:
- You need to reference by a specific, unchanged file name (e.g., `favicon.ico`, `robots.txt`).
- You have thousands of images and don't want to import them all, accepting the trade-off of no build-time optimization.
- Are referenced from outside your React code, like in the `index.html` file itself.
Downside: These files are not processed. This means they are not minified, their filenames are not hashed for cache-busting, and if the file is missing, you won't get a build error—it will just result in a 404 error in production.
Method 2: Importing from `src` - The Recommended Path
For most images tied to your components (logos, icons, user-generated content placeholders), this is the superior and recommended approach for 2025.
How It Works
When you import an image from your `src` directory, the bundler includes that image in its dependency graph. It will process the image and, during the build, copy it to the `build` folder with a unique, hashed filename (e.g., `logo.a8h3b4n.png`). The `import` statement doesn't give you the image data; it gives you a string representing the final, public path to that processed image.
Place your image in `src/assets/logo.svg`. Then, in your component:
import React from 'react';
import logo from '../assets/logo.svg'; // The path is relative to the current file
function Header() {
return (
<header>
<img src={logo} alt="Application logo" />
</header>
);
}
export default Header;
Why It's Better
- Error Prevention: If the image `logo.svg` is moved, deleted, or misspelled, your application will fail to compile. This prevents broken images from ever reaching production.
- Cache Busting: The generated filename includes a unique hash. If you update the image, the hash changes, forcing the browser to download the new version instead of serving a stale, cached one.
- Co-location: You can keep images directly related to a component in the same folder (`/components/UserProfile/UserProfile.js` and `/components/UserProfile/avatar-placeholder.png`), making your project structure more modular and maintainable.
- Optimization: Some setups can be configured to automatically optimize images (like compression) during the build process when they are imported.
Handling Dynamic Images in 2025
What if the image you need to display depends on a prop or state? You can't use a static `import` at the top of the file. Here's how to handle it in both CRA and Vite environments.
The Legacy Way: `require()` in Create React App
In older, CommonJS-heavy environments like the default CRA setup, `require()` is your tool for dynamic imports inside a component. Webpack sees `require()` and understands that it needs to bundle everything that could possibly match the path.
function UserAvatar({ iconName }) { // e.g., iconName = 'user-female'
// Note: The path must be partially static for Webpack to know where to look.
const imagePath = require(`../assets/icons/${iconName}.png`);
return <img src={imagePath} alt={`Icon for ${iconName}`} />;
}
The main caveat is that the path inside `require()` cannot be a completely dynamic variable. Part of the path must be a static string so the bundler can know which directory to inspect and bundle.
The Modern Way: `new URL()` in Vite
Vite, being ESM-native, uses a different, more web-standard pattern. It leverages the `URL` constructor, a standard browser API.
function UserAvatar({ iconName }) { // e.g., iconName = 'user-female'
const getImageUrl = (name) => {
// import.meta.url gives the URL of the current module.
// The new URL() constructor resolves the relative path of the image against the module's URL.
return new URL(`../assets/icons/${name}.png`, import.meta.url).href;
};
return <img src={getImageUrl(iconName)} alt={`Icon for ${iconName}`} />;
}
This approach is powerful because it's explicit and uses platform features. Vite detects this pattern and correctly bundles the assets, replacing the code with the correct paths during the build.
Comparison: `public` vs. `src` vs. Dynamic Methods
Method | How to Use | Pros | Cons | Best For |
---|---|---|---|---|
`public` Folder | `<img src="/image.png">` | Simple, no imports, predictable URL. | No build-time checks, no cache-busting hash, can't be optimized. | Global assets like `favicon.ico`, `manifest.json`. |
`import` from `src` | `import img from './img.png'; <img src={img}>` | Build-time error checking, cache-busting, co-location with components. | Requires an `import` for each asset, verbose for many images. | Component-specific images (logos, icons, UI elements). The default choice. |
Dynamic (`require` / `new URL`) | `src={require(path)}` or `src={new URL(path, ...).href}` | Allows image paths to be determined by props or state. | More complex syntax, potential to bloat bundle if not used carefully. | Displaying an image from a list or based on user interaction. |
Common Pitfalls and Quick Fixes
- Problem: Using a relative path for a `public` folder image (`src="images/logo.png"`).
Fix: Always use an absolute path from the root (`src="/images/logo.png"`). - Problem: Trying to use a string path for a `src` folder image (`src="../assets/logo.png"`).
Fix: You MUST `import` the image first and use the resulting variable in your `src` attribute. - Problem: Dynamic `import()` doesn't work for images.
Fix: Dynamic `import()` is for code-splitting JS modules, not for asset paths. Use the `require()` or `new URL()` patterns instead. - Problem: My background images in CSS aren't working.
Fix: The same rules apply. In a CSS file inside `src`, use a relative path (`background-image: url('../assets/background.jpg');`). The bundler will process this correctly. - Problem: Case sensitivity. Your code has `logo.PNG` but the file is `logo.png`.
Fix: This works on case-insensitive systems (macOS, Windows) but will break on case-sensitive systems (Linux, used by most build servers). Always ensure your file names and code match exactly.