Reputation: 417
Recently, I have been working on a project in NextJS which uses the YoutubeAPI to fetch video information, including thumbnail URLs.
The thumbnail URL for a full resolution image looks like this:
https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg
However, sometimes YouTube fails to generate a full-resolution image, and in that case, the image is not displayed on my webpage.
In the case that the image with the URL https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg
does not exist I wish to use another URL like https://i.ytimg.com/vi/${videoId}/hqdefault.jpg
What is the best way to handle this with next/image
?
Upvotes: 39
Views: 49569
Reputation: 267
I am using this component in Next.js version 14.2.5 as of August 2024:
"use client"
import type { ImageProps } from 'next/image'
import Image from 'next/image';
import type React from 'react';
import { useEffect, useState } from 'react';
import fallbackImage from '@/assets/images/placeholder.jpg'
interface ImageFb extends ImageProps {
fallbackSrc?: string
}
const ImageFb: React.FC<ImageFb> = ({ src, fallbackSrc = fallbackImage.src, ...rest }) => {
const [imgSrc, set_imgSrc] = useState(src);
useEffect(() => {
set_imgSrc(src);
}, [src]);
return (
<Image
src={imgSrc}
onLoad={(result) => {
if (result.currentTarget.naturalWidth === 0) {
set_imgSrc(fallbackSrc); // Fallback image
}
}}
onError={() => {
set_imgSrc(fallbackSrc);
}}
placeholder='blur'
blurDataURL={fallbackSrc}
{...rest}
/>
);
}
export default ImageFb
Upvotes: 0
Reputation: 925
Vercel now has official example for this scenario
https://vercel.com/templates/next.js/image-fallback
const ImageWithFallback = ({
fallback = fallbackImage,
alt,
src,
...props
}) => {
const [error, setError] = useState(null)
useEffect(() => {
setError(null)
}, [src])
return (
<Image
alt={alt}
onError={setError}
src={error ? fallbackImage : src}
{...props}
/>
)
}
Upvotes: 1
Reputation: 11
'use client'
import Image from 'next/image'
import React, { useState } from 'react'
const ImageToast = ({image}: any) => {
const [imgSrc, setImgSrc] = useState(false);
return (
<Image
src={imgSrc ? image : '/img/loader.jpg'}
onError={() => setImgSrc(true)}
alt = ''
fill
/>
)
}
export default ImageToast
Upvotes: 1
Reputation: 773
<Image
loader={({ src }) => src}
src={imageUrl ? imageUrl : "/images/no-thumbnail.jpg"}
width={500}
height={500}
alt={imageUrl ? imageUrl : "/images/no-thumbnail.jpg"}
onError={(event) => {
event.target.id = "/images/no-thumbnail.jpg";
event.target.srcset = "/images/no-thumbnail.jpg";
}}
/>
onError you can set image srcset. By image srcset nextjs image show images.
Upvotes: 6
Reputation: 41
This is my attempt for building an <Avatar>
parent component with <Avatar.Image>
and <Avatar.Fallback>
child components.
Firstly, I built the <Avatar.Image>
component to display the image of our avatar.
import React, { useState } from 'react';
import Image from 'next/image';
function AvatarImage({ src, alt, className, ...props }) {
const [imageSuccessfullyLoaded, setImageSuccessfullyLoaded] = useState(true);
return (
<div>
{imageSuccessfullyLoaded && (
<Image
src={src}
alt={alt}
placeholder="empty"
onError={() => {
setImageSuccessfullyLoaded(false);
}}
fill
{...props}
/>
)}
</div>
);
}
export default AvatarImage;
Once the AvatarImage
component fails to load, I let the AvatarFallback
component fill the background.
import React from 'react';
import classNames from 'classnames';
function AvatarFallback({ children, className }) {
return (
<div
className={classNames(
'text-lg font-medium text-neutral-600 dark:text-neutral-200',
className
)}
>
{children}
</div>
);
}
export default AvatarFallback;
The parent component is named Avatar
import React from 'react';
import classNames from 'classnames';
import AvatarImage from './AvatarImage/AvatarImage';
import AvatarFallback from './AvatarFallback/AvatarFallback';
function Avatar({ className, children }) {
return (
<div
className={classNames(
'relative flex items-center justify-center overflow-hidden rounded-full bg-neutral-200 dark:bg-neutral-600',
className
)}
>
{children}
</div>
);
}
Avatar.Image = AvatarImage;
Avatar.Fallback = AvatarFallback;
export default Avatar;
Below is an example of how I use it:
(don't forget to change ${videoId}
)
<Avatar>
<Avatar.Image src="https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg" alt="Avatar image" />
<Avatar.Fallback>
<Image
src="https://i.ytimg.com/vi/${videoId}/hqdefault.jpg"
fill
alt="Avatar image"
/>
</Avatar.Fallback>
</Avatar>
Upvotes: 1
Reputation: 1834
None of the above solutions worked for me. But, it worked when I used "onErrorCapture" instead of "onError"! =)
Upvotes: 6
Reputation: 171
There is the @juliomalves solution with typescript
import React, { useState } from 'react';
import Image, { ImageProps } from 'next/image';
interface ImageWithFallbackProps extends ImageProps {
fallbackSrc: string
}
const ImageWithFallback = (props: ImageWithFallbackProps) => {
const { src, fallbackSrc, ...rest } = props;
const [imgSrc, setImgSrc] = useState(src);
return (
<Image
{...rest}
src={imgSrc}
onError={() => {
setImgSrc(fallbackSrc);
}}
/>
);
};
export default ImageWithFallback
Upvotes: 16
Reputation: 543
These answers were helpful but there's a way to achieve this without needing to pass a key
each time by taking advantage of the useEffect
hook:
useEffect(() => {
set_imgSrc(src);
}, [src]);
Additionally, the onError
event doesn't seem to trigger for certain images (I believe layout='fill'
doesn't trigger it in certain scenarios), for those cases I've been using the onLoadingComplete
and then I check if the width of the image is 0
onLoadingComplete={(result) => {
if (result.naturalWidth === 0) { // Broken image
set_imgSrc(fallbackSrc);
}
}}
Full code:
import Image from "next/image";
import { useEffect, useState } from "react";
export default function ImageFallback({ src, fallbackSrc, ...rest }) {
const [imgSrc, set_imgSrc] = useState(src);
useEffect(() => {
set_imgSrc(src);
}, [src]);
return (
<Image
{...rest}
src={imgSrc}
onLoadingComplete={(result) => {
if (result.naturalWidth === 0) {
// Broken image
set_imgSrc(fallbackSrc);
}
}}
onError={() => {
set_imgSrc(fallbackSrc);
}}
/>
);
}
Upvotes: 28
Reputation: 50338
You can create a custom image component that extends the built-in next/image
and adds the fallback logic if the image fails to load by triggering the onError
callback.
import React, { useState } from 'react';
import Image from 'next/image';
const ImageWithFallback = (props) => {
const { src, fallbackSrc, ...rest } = props;
const [imgSrc, setImgSrc] = useState(src);
return (
<Image
{...rest}
src={imgSrc}
onError={() => {
setImgSrc(fallbackSrc);
}}
/>
);
};
export default ImageWithFallback;
Then, you can directly use the custom component instead of next/image
as follows:
<ImageWithFallback
key={videoId}
layout="fill"
src={`https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`}
fallbackSrc={`https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`}
/>
Passing a key
prop to trigger a re-render on videoId
change.
Upvotes: 64
Reputation: 171
@juliomalves has given 99% percent of the answer, however I would like to add to it. There is a problem when changing the src in his solution, as the image would not update because it's getting the imgSrc value which is not getting updated. This is my addition to his answer:
import React, { useState } from 'react';
import Image from 'next/image';
const ImageFallback = (props) => {
const { src, fallbackSrc, ...rest } = props;
const [imgSrc, setImgSrc] = useState(false);
const [oldSrc, setOldSrc] = useState(src);
if (oldSrc!==src)
{
setImgSrc(false)
setOldSrc(src)
}
return (
<Image
{...rest}
src={imgSrc?fallbackSrc:src}
onError={() => {
setImgSrc(true);
}}
/>
);
};
export default ImageFallback;
Now imgSrc is used only as a flag, and there is tracking of src value, which helps to change the image even if you had an image that had the fallback image on before.
Upvotes: 10