23

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?

juliomalves
  • 42,130
  • 20
  • 150
  • 146
m_ognjen
  • 255
  • 1
  • 2
  • 8

6 Answers6

48

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.

juliomalves
  • 42,130
  • 20
  • 150
  • 146
20

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);
      }}
    />
  );
}
ralrom
  • 433
  • 3
  • 9
  • I'd argue about this being easier than passing a `key`. Still, changing the `src` inside a `useEffect` will trigger a re-render too. It doesn't really address the re-rendering issue, unless I'm misunderstanding what you meant. – juliomalves Jan 01 '22 at 13:22
  • Yes we want to cause a re-render when the src changes. (It's not an issue, edited my answer to clarify) With your method you must pass the `key` each time you call ``; if you forget to do it somewhere, the image won't re-render if the `src` changes. With the `useEffect` hook inside the `` component, there's nothing to forget because it's inside the internal logic of the component itself. Both achieve the same thing differently, and even though a `key` is "easier" to implement, it adds work for you as a developer each time you want to use the component. – ralrom Jan 02 '22 at 22:05
  • 1
    Fair enough, thanks for clarifying. – juliomalves Jan 03 '22 at 10:32
  • thanks, you save my live – volos Sep 06 '22 at 15:12
10

@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.

Panos Paschalis
  • 171
  • 2
  • 13
  • 8
    This could easily be solved by passing a `key` prop (based off `src`/`fallbackSrc`) to `ImageWithFallback` in my example. See my updated answer. – juliomalves Apr 23 '21 at 08:56
7

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

5

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

Noby Fujioka
  • 1,744
  • 1
  • 12
  • 15
1

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>
Baran
  • 31
  • 3