# react-image: an img tag that survives a failed load

> react-image is an img tag replacement and a useImage hook for React that tries a list of image sources in order, showing a loader while loading and an unloader when every source fails. It has no external dependencies, works with Suspense, and the standalone Img component is documented as legacy in favour of the hook.

**mbrevda/react-image** — React.js <img> tag rendering with multiple fallback & loader support

- Repository: https://github.com/mbrevda/react-image
- Stars: 1,232 · Forks: 89
- Language: TypeScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/mbrevda-react-image

## The problem is an img tag that renders a broken image icon

react-image is described as an img tag replacement and hook for React.js, supporting fallback to alternate sources when loading an image fails, and the whole library is a response to one small failure that is more annoying than it sounds.

A plain `<img src=...>` has no failure state. If the URL 404s, the browser shows a broken image placeholder, and in some layouts that placeholder is larger than the image ever was, and it is a different shape, so the layout shifts after the fact. The library's answer is stated plainly: if the image cannot be loaded, the img element will not be rendered, preventing a broken image from showing.

The second half of the problem is that one URL is not always enough. The library allows one or more images to be used as fallback images in the event that the browser could not load the previous image, and when src is specified as an array it attempts to load all of them, starting at the first and continuing until an image has been successfully loaded.

That is the case the library is really for. A CDN that is misbehaving for a subset of users, an image host that has migrated, a URL that needs a transformation applied on the fly and the transformation service occasionally returns an error, all of these have the same fix, and the fix is a second URL.

The dependency story is worth stating because it is unusual for a component library of this size. react-image has no external dependencies, aside from a version of react and react-dom which support hooks, and @babel/runtime. The repository topics carry the older name the package was published under, react-img-multi, which is a clue that the API has been stable long enough to have accumulated aliases in search.

It is MIT licensed, the version is 4.1.0, the last push was on 2026-10-14, and the repository has no GitHub releases, so the npm version is the version to pin against.

## The hook is the recommended API and the component is legacy

The documentation is explicit about which one to use. When possible, you should use the useImage hook, because it provides greater flexibility and provides support for React Suspense, and the standalone component is labelled legacy in the section heading.

The hook encapsulates all the image loading logic, and by default it works with React Suspense, suspending painting until the image is downloaded and decoded by the browser.

```js
import React, {Suspense} from 'react'
import {useImage} from 'react-image'

function MyImageComponent() {
  const {src} = useImage({
    srcList: 'https://www.example.com/foo.jpg',
  })

  return <img src={src} />
}
```

The API surface is small and each option is worth reading.

`srcList` is a string or an array of strings, and useImage tries them one at a time and returns after the first one is successfully loaded.

`imgPromise` is a promise that accepts a url and returns a promise which resolves if the image is successfully loaded or rejects if it does not. The documentation suggests injecting an alternative implementation for advanced custom behaviour, such as logging errors or dealing with servers that return an image with a 404 header, and that last case is the important one. A server that responds with a 200 status and a broken body, or with a 404 and an image payload, defeats a naive load check, and the imgPromise hook is where you fix that.

`useSuspense` is a boolean, and by default useImage will tell React to suspend rendering until the image is downloaded. Suspense can be disabled by setting this to false.

The hook returns three things. `src` is the resolved image address, `isLoading` is the current loading status, with the note that this is never true when using Suspense, because with Suspense the component is not rendered at all while loading, and `error` is any errors encountered.

The hook throws an error if it fails to find any images, and the documentation tells you to wrap your component with an Error Boundary to catch that scenario and do or show something. So the failure path is two-layered: Suspense for the loading state, and an error boundary for the total-failure state.

## The Img component, and the four props that only exist on it

The standalone component keeps the older, more declarative API, and the documentation is careful to mark which options do not apply to it.

The base usage is a src prop that accepts either a string or an array:

```js
import {Img} from 'react-image'

const myComponent = () => <Img src="https://www.example.com/foo.jpg" />
```

which resolves to a plain img element with the same src.

The loader prop takes any valid React element and is shown before the image has loaded, and the unloader prop takes any valid React element and is shown if none of the images could be loaded. The library itself is called a spinner in the prose, but the prop takes any element, so a skeleton placeholder in your own design system works just as well.

There is a caching behaviour in both. If an image was previously loaded successfully, since the last time the page was loaded, the loader will not be shown and the image will be rendered immediately instead. And if an image has previously been attempted unsuccessfully, react-image will not retry loading it again until the page is reloaded.

That second rule is a design decision with consequences. It stops the library from repeatedly hammering a URL that already failed, which is good for the failing case. It also means that a transient network failure is sticky for the lifetime of the page, so a user on a flaky connection who hits one failed attempt will not get a retry, and neither will the next attempt at a different source if that one is in the same failed set. The cache is per page load, not per session, which keeps it from growing without bound, but it also means the behaviour of the fallback chain depends on the order and timing of the failures.

The note at the end of the component section is important for anyone choosing between the two APIs: the following options only apply to the Img component, not to the useImage hook, and when using the hook you can inject a custom image resolver with custom behaviour as required.

## Decode before paint, and the CORS attribute

Two of the Img component's props exist for reasons that are not obvious from their names, and both are about the browser rather than about React.

The first is `decode`. By default and when supported by the browser, react-image uses `Image.decode()` to decode the image and only render it when it is fully ready to be painted. The documentation explains why it matters: while this does not matter much for vector images such as SVGs which are rendered immediately, decoding the image before painting prevents the browser from hanging or flashing while the image is decoded. If the behaviour is undesirable it can be disabled by setting the decode prop to false.

That is a real class of bug that the decode step fixes. A large JPEG can take tens of milliseconds to decode, and if the browser starts that work in the middle of a frame, the frame is delayed. On a page with several images, the flashing is visible. Setting decode to false is the escape hatch, and it makes sense for a small icon or an image that is already in cache.

The second is `crossorigin`. When loading images from another domain with a CORS policy, you may find you need to use the crossorigin attribute, for example:

```js
const myComponent = () => (
  <Img src={'https://www.example.com/foo.jpg'} crossorigin="anonymous" />
)
```

The reason to mention it here is that this is one of the props you cannot get from a plain img element by accident. If you set crossorigin on a plain img and the server does not send the CORS headers, the image fails to load entirely, and the library treats that failure exactly like any other, so it moves to the next source in the list. If your fallback chain mysteriously skips sources, that is the first thing to check.

There is also a container wrapper prop for higher level operations beyond the scope of the project, and it takes a single property, children, which is whatever is passed in.

## The build and test setup behind the package

The repository layout shows a TypeScript source tree with a custom build script rather than a bundler configuration, and the test story is the part that is most informative about how the library is maintained.

The build script is `node build.js && npm run build:types && mv dist/src/*.d.ts dist`, so the JavaScript output is produced by a hand-written script, and the type declarations are generated separately with `tsc -p tsconfig.json --emitDeclarationOnly` and then moved into place. The package publishes three entry points: `module` points at `dist/esm/index.js`, `main` points at `dist/cjs/index.js`, and `types` points at `dist/index.d.ts`, with the package marked as `type: module`. The published `files` list is the dist ESM and CJS JavaScript, the markdown files and the declaration files, and it explicitly excludes the test files.

The interesting script is `test:dist`, which copies the test files into both built output directories, writes a CommonJS package.json into the CJS one, and then runs the test suite against the built package rather than the source. This is the kind of check that catches packaging mistakes which source-level tests cannot, such as a missing export in the CJS build or a file that was never included in the `files` list, and it is a good sign of a maintainer who has been bitten by that class of bug.

The rest of the tooling is conventional and modern. Jest with a `jest.config.cjs` and a `jest.setup.ts`, an `.eslintrc`, a `.prettierrc`, `renovate.json` for dependency updates, a `CHANGELOG.md`, a `CONTRIBUTING.md`, an `UPGRADE.md` and a `LICENSE`. The `dev/` directory holds the development harness and `build.js` is at the root.

The `UPGRADE.md` file is worth reading if you have used an earlier version, because the hook replaced the standalone component as the recommended API and the two do not share a prop surface, and the repository documents those differences rather than leaving you to find them.

## Where react-image is the wrong choice

The first case is a project that wants image optimisation. react-image does not resize, does not negotiate formats, does not generate responsive `srcset` markup, and does not lazy load. It renders the URL you give it, once it has loaded. If your requirement is a properly optimised image pipeline, a component that transforms the URL, or a `srcset` set of candidates, this is the wrong library and you want an image component or a build-time image pipeline instead.

The second case is a project that already has a loading strategy. If you use a framework-level lazy loading or an image CDN with its own client-side API, the loader prop and the decode behaviour here are solving a problem you have already solved, and adding this library means two systems coordinating over the same images. The `useSuspense` option at least means you can opt out of the Suspense part of the behaviour and keep the fallback chain.

The third is a project that wants server rendering without Suspense. The Suspense default is what makes the hook elegant, and it is also a constraint: the component does not render until the image is ready, so the HTML the server produces contains no image at all. For a content site where the image matters for the initial paint and for search, an img tag in the initial HTML is often what you want, and this library is built the other way round. The standalone component with a loader is the closer fit, but the documentation calls it legacy, which tells you where the author thinks the project is going.

There is a fourth, smaller point about the failed-image cache. A source that failed once will not be retried until the page reloads, so a user whose first attempt failed through a transient error keeps the fallback image for the rest of the session. For a long-lived single-page application that is a visible behaviour, and the fix is to pass a custom `imgPromise` or to reload.

One last point on the platform support, which the README does not discuss at length. The library uses `Image.decode()`, which is not available in every browser, and the fallback chain relies on the browser's image loading behaviour to detect a failure. The documentation says the decode behaviour applies when supported by the browser, and disables gracefully otherwise, but the failed-image detection itself depends on the browser firing the error event, and there is no statement in the README about which browsers are covered.

## Conclusion

react-image fits an application whose image URLs are unreliable, whether because they are served from a CDN that fails, are transformed by a service that returns errors, or simply 404 on a percentage of requests, and it fits a team that wants the fallback behaviour in one place rather than in every image component. It does not fit a project that wants image optimisation, since it renders whatever URL you give it and does no resizing, format negotiation or lazy loading of its own. Before you adopt it, read the UPGRADE.md file at the root of the repository, because the hook replaced the standalone component and the two have different props.

## FAQ

### How do I install react-image?

Run `npm install react-image --save` for ES6, CommonJS or UMD locally, or load it globally from a CDN with `<script src="https://unpkg.com/react-image/umd/index.js"></script>`. It has no external dependencies beyond a version of react and react-dom that support hooks, and @babel/runtime.

### How do I give an image multiple fallback sources?

Pass an array to src, either on the standalone `Img` component as `src={['https://www.example.com/foo.jpg', 'https://www.example.com/bar.jpg']}` or to the hook as `srcList`. The library tries them in order and stops at the first that loads successfully.

### What is the difference between the useImage hook and the Img component?

The hook is the recommended API and supports React Suspense, suspending rendering until the image is decoded, and it throws an error when every source fails, which you catch with an error boundary. The standalone component is marked legacy, and options like `decode` and `crossorigin` apply only to it, not to the hook.

### How do I show a spinner or placeholder while the image loads?

On the standalone component, pass a `loader` element for the loading state and an `unloader` element for the case where no image could be loaded. With the hook, wrap the component in `<Suspense>` and give it a `fallback` prop instead.

## Sources

- [Issues](https://github.com/mbrevda/react-image/issues)
- [License: MIT](https://github.com/mbrevda/react-image/blob/master/LICENSE)
- [mbrevda/react-image on GitHub](https://github.com/mbrevda/react-image)
- [README](https://github.com/mbrevda/react-image/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mbrevda-react-image
