😺

Chakra UIのImageのloading属性と正しく付き合う

2021/12/26に公開

loading属性とfallback属性は同時に使えない

Chakra UIのImageコンポーネントでは、loading属性をつけるとfallback属性およびfallbackSrc属性が無視されます。
https://github.com/chakra-ui/chakra-ui/issues/1027

例えば、以下のようなコードを書いてもfallbackSrcに指定した画像は表示されません。

<Image
  src="/image-not-exist.png"
  fallbackSrc="https://via.placeholder.com/100?text=fallbackSrc"
  loading="lazy"
/>

バグなのか?

バグではないでしょう。
ドキュメントに記載はないものの、先述のIssueで明記していることから、意図した実装だろうと判断できます。

ではなぜこのような実装になっているか。以下、推測です。

fallback/fallbackSrcは、ドキュメント上は「srcの読み込み失敗時に代わりに表示」となっています。

You can provide a fallback image for when there is an error loading the src of the image.

一方で、テストケース上は「srcの読み込み失敗時または読み込み中に表示」となっています。実装と一致します。

Chakra has support for fallback images so in event the image falls to load, or while the image is loading, you can show a fallback.

おそらく、テストケース側が期待する振る舞いでしょう。
そして、loading="lazy" がついていた場合にfallback/fallbackSrcをどう取り扱うかが定まらず(読み込み中の定義が定まらない)、fallback/fallbackSrcを無視してImageコンポーネントの利用者に相応の実装を任せることにしたのではないかと思います。

本来期待される振る舞いは何なんだ、という確認をとりたいためIssueを切りました。この記事を書いている時点では返答は来ていません。
https://github.com/chakra-ui/chakra-ui/issues/5288

loading="lazy"としながらfallbackも指定したい

利用者の体験を手軽に向上させられるため、loading="lazy" は積極的に使っていきたいわけです。[1]
そして、読み込み失敗時のfallbackも用意したいケースも多いでしょう。

loading="lazy" でありながら、srcの読み込み失敗時にfallbackを表示するには以下のようなコンポーネントを作ると良いでしょう。

import { ReactEventHandler, useEffect, useState } from "react";
import { ChakraProvider, Image, ImageProps } from "@chakra-ui/react";

const LazyImage = ({
  src,
  fallback,
  fallbackSrc,
  onError,
  ...rest
}: ImageProps) => {
  const [imgSrc, setImgSrc] = useState(src);
  const [returnFallback, setReturnFallback] = useState(false);
  useEffect(() => {
    setImgSrc(src);
    setReturnFallback(false);
  }, [src]);
  const _onError: ReactEventHandler<HTMLImageElement> = (e) => {
    if (fallback) {
      setReturnFallback(true);
    } else if (fallbackSrc) {
      setImgSrc(fallbackSrc);
    }
    onError?.(e);
  };
  if (returnFallback) {
    return fallback!;
  }
  return <Image {...rest} src={imgSrc} loading="lazy" onError={_onError} />;
};

export default function App() {
  return (
    <ChakraProvider>
      <LazyImage
        src="/image-not-exist.png"
        fallbackSrc="https://via.placeholder.com/100?text=fallbackSrc"
      />
    </ChakraProvider>
  );
}

なお、上記実装ではsrcの読み込み中にはfallback/fallbackSrcが表示されません。
画像の読み込み開始のイベントを取得することができないためです。[2]
もしこの実装を行うのであれば、loading="lazy" に頼らず、Intersection Observer APIを使って loading="lazy" 相当の実装を自前でやることになるでしょう。

脚注
  1. iOSのSafariでは loading="lazy" がexperimental feature扱いで、通常は無効化されていることに注意が必要。 ↩︎

  2. img.onLoadStartは無効化されている。 https://github.com/facebook/react/issues/20330 ↩︎

Discussion