m_ognjen
m_ognjen

Reputation: 417

What is the best way to have a fallback image in NextJS?

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

Answers (10)

MRAH
MRAH

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

eliezra236
eliezra236

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

alex kiruri
alex kiruri

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

Chandrika Prasad Shah
Chandrika Prasad Shah

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

Baran
Baran

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

Noby Fujioka
Noby Fujioka

Reputation: 1834

None of the above solutions worked for me. But, it worked when I used "onErrorCapture" instead of "onError"! =)

Upvotes: 6

Jeremy Andress
Jeremy Andress

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

ralrom
ralrom

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

juliomalves
juliomalves

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

Panos Paschalis
Panos Paschalis

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

Related Questions