next 14 Showing image fallback on public folder image 404
I am using next 14, and storing some images in my app's
public
folder. These images are then requested by <img>
elements in some of my client components.
If the requested resource does not exist, I would like to show a 'default image' which is effectively a file that is guarnateed to exist in the public folder.
To try and do this I have written a simple client component that wraps the img element.
However, the 'onError' callback is never fired. I believe this is because althought Next is in fact responding with 404
it is also sending a response body. (the default not-found HTML).
I have attached images showing that when the image content is requested, as stated the server responds with a 404
, however also sends content in the response body. I wonder if because there is techincally a 'response' the image element is trying to display the response body content (the HTML) as the content of the image, and thus the onError callback does not fire.
Can anyone think of an alternative approach / fix to present a different image on image load fails from the public folder. Perhaps a different callback attribute on the img element could be leveraged and we could somehow check if the response was a 200? (or atleast not 404) ?10 Replies
You need to guarantee that
onerror
is set before src
is set. Check out the useEffect
I added here that properly shows the error:
https://stackblitz.com/edit/nextjs-ygikmj?file=app%2FMyImage.tsx,app%2Fpage.tsxI mean you could just do
src={activeSrc || "fallbackImg.png"}
I made some slight changes as it didn't seem to work for me without them. However, this general approach definitely does seem to work.
However, it is worth noting that if the original image (non-fallback) src is not present, the client seems to make 2 requests which will both fail to the server - ideally we would only make one. However, I'm not sure if this is a caveat that only occurs during strict mode. Also - could you explain some more about what the
new Image()
contructor does? It seems to me that it initialises a new HTML dom element, however do we even use the element if this is the case? It doesn't seem we reference the const again outside of the useEffect callback. If it is a new dom element could we somehow save it in state so the .src call is only made once?
This wouldn't work. The question is such that if the resource located at the path defined by activeSrc
doesn't exist then resort to the fallback image. Hence, even if activeSrc
is defined and is truthy, the resource specified by the string provided might not exist. This is the case we want to handle.it is a new
<img />
, yes - this is a common approach from long before React was ever around so that the user doesn't see what's going on
if you're seeing two requests, only the first should actually be made and any additional ones should be coming from your disk cache
you could also tweak this to use only a single <img />
via a ref and destructuring the props better so that you're directly assigning onError
and src
through the ref and jsx only assigns the other ones, but then I'd also do something to hide the image until the end so it doesn't flash the broken stateHow would you reccomended potentially implemented such a fix in regards to not flashing the broken state? Also what do you mean by ‘destructuring the props better’ ?
To not flash the broken state I'd use
visibility: hidden
until you have a working image in there - this way if a height/width are given then you avoid the layout shift that comes with other options (using the NextJS <Image />
component also helps here and might be a better abstraction than <img />
)
Sorry "better" wasn't a good word choice. I was skimming over the code and there are a number of things that could improve code clarity and functionality:
- {...props}
looks like all props but it's not, I'd name this restProps
since you're already taking src
and fallbackSrc
out of what would be the full props
(this is where I thought you weren't destructuring at all when I first looked)
- I'd destructure onError
as well and call it if given, or exclude it from the type (I'm assuming it's part of ImgHTMLAttributes<HTMLImageElement>
- once you are setting visibility: hidden
while it loads, you'll need to ensure you still respect any given props.styles
- you could also use a single useEffect
, having one effect to set a variable to trigger another one delays things for an entire render cycle and uses 2 renders to do what a single one could do
- you're not currently handling if src
or fallbackSrc
changeApologies, I misunderstood your question and thought perhaps we were over complicating things.
no worries 👍
I like the visibility hidden suggestion, thanks. However I am not sure how you suggest we could 'use only a single <img /> via a ref'. I asumme you mean to use the
useRef
hook like so const imageRef = useRef<HTMLImageElement>(null);
and then bind it to the returned img. Would you then suggest somehow mutating the imageRef.current
to use the desired onError callbacks? If this is the case, does this not also fall into the pitfall that the onError callback is only bound after the image has already tried to fetch the image source?
i've come up with this updated component:
I believe it makes use of most of the suggested improvements you provided. However - I'm not sure I've implemented the ref
part you suggested.
Also, I am currently omitting the onError
prop as it expected an event argument. I would rather like to not omit this and instead call the provided callback in addition to the component required behaviour, however when I attempted to give dummy.onerror
an argument e
typescript didn't seem to know what type this argument would be. I wonder if this is because I am redefining the callback and if I should specify that e
is of type image event
(or whatever the correct type is).getting there, couple issues I see:
- nothing updates
activeSrc
when src
changes
- nothing sets hasLoaded
back to false
if it needs to load something new
- ...restProps.style
should be listed first so your override wins
I'm not sure how to better explain what I meant by using a ref
without showing the code so I updated my demo.
- I added a small test bed to page.tsx
so I can play with it.
- Mine updates when props change
- but the visibility
flash on a cached image from handling the props updating is pretty noticeable in a side-by-side comparison. It'd need more testing. Maybe it's fine in a real world scenario. Maybe something like requestAnimationFrame(() => if (status === 'loading') visibility = 'hidden')
to delay hiding it long enough to check if it will load immediately or not might help. Maybe it's not necessary at all (I was thinking to prevent seeing a "broken image" but in testing so far I'm not seeing one regardless - maybe onerror
swapping the img.src
prevents it from flashing, at least in Chrome.)
- And I also hit the TS error on the event
object, I didn't spend time figuring it out.
- But you can see how to use the ref to do everything without the dummy
img element if nothing else.
https://stackblitz.com/edit/nextjs-ygikmj?file=app%2FMyImage.tsx,app%2FImageWithFallback.tsx,app%2Fpage.tsxThank you once again. I see what you mean now with using the ref, I was confused as in my head i was adiment with using the activeSrc state and passing that to the react element as the
src
prop - hence I thought we'd just encounter the original issue of onError firing after src starts loading.
In regards to the event typing mismatch I've found that apparently React wraps native events in a wrapper so they behave the same way in all browsers https://legacy.reactjs.org/docs/events.html . I'm not entirely sure if there is a way to convert from a DOM event to this wrapper, (https://stackoverflow.com/questions/65889954/how-to-convert-a-raw-dom-event-to-a-react-syntheticevent) hence I think for now I'm just going to omit the onLoad and onError props from the component.
In regards to the image flashing, again, It is not ideal - however, I think it is bearable. I am also not familiar with requestAnimationFrame
so I wouldn't be able to do much using this right now. I also can't think of any other means of solving the issue.SyntheticEvent – React
A JavaScript library for building user interfaces
Stack Overflow
How to convert a "raw" DOM Event to a React SyntheticEvent?
I use two libraries, one library which emits "raw" DOM events (lib.dom.d.ts), and one other library which consumes React.SyntheticEvents.
What is the best way to cleanly transform the raw...