🖼️

Next.js next/imageのheightとwidthを自動判定する

2023/04/30に公開

はじめに

Next.jsでの開発時に、画像のアスペクト比を保持しながら、要素のサイズを自動的に調整したい(widthとheightを明示的に指定したくない)場面があります。

問題

一般的には、Imageコンポーネント(バージョン13以降)でfill属性を指定し、styleでobjectFit:containを設定する方法が考えられます。しかし、この方法ではImageコンポーネント自体がposition:absolute属性を持つimg要素に変換されるため、親要素にwidthとheightを持たせる必要があり、根本的な解決には至りません。
一部の記事では、CSSで!importantを使ってposition:absoluteを解除する方法が紹介されていますが、現行バージョンでは警告が表示されます。

結論

コンポーネントを自作する

!importantでフレームワークのスタイル指定を打ち消すということ自体、
一体なんのためにフレームワークを使っているのか…と虚無な気持ちになるわけですが、
ざっと検索したところではNext.js環境で完結させる前提でこれが一番だと言えるようなソリューションは見当たりませんでした。
よって、コンポーネントを自作することにしました。

以下、コードです。

コード

// 未定義サイズの宣言
const EMPTY_SIZE = {
    width: 0, height: 0
};

export default function ImageContainer({ src, alt, className, width, height }) {
    const [wrapperSize, setWrapperSize] = useState({
        width: "auto", height: "auto"
    });

    const wrapper = useRef(null);

    useEffect(() => {
        // 読み込み時、ラッパーのサイズを取得する
        const { width, height } = wrapper.current.getBoundingClientRect() || EMPTY_SIZE;

        // 縦横ともに値が自動算出済みだった場合、そのまま適用する
        if (width > EMPTY_SIZE.width && height > EMPTY_SIZE.height) {
            return;
        }

        const image = new Image();
        image.src = src;

        image.onload = () => {
            // 縦横ともに値が未確定である場合、画像のサイズをそのままラッパーに適用する
            if ((height + width) == 0) {
                setWrapperSize({ height: image.naturalHeight, width: image.naturalWidth });
                return;
            }

            // どちらか片方でもラッパーのサイズが確定している場合、もう片方の値を自動算出する
            const computedHeight = height == EMPTY_SIZE.height ? (image.naturalHeight * width) / image.naturalWidth : height;
            const computedWidth = width == EMPTY_SIZE.width ? (image.naturalWidth * height) / image.naturalHeight : width;

            // ラッパーのサイズを確定
            setWrapperSize({ height: computedHeight, width: computedWidth });
        };

        image.onerror = () => {
            console.error('Error loading the image');
        };
    }, [wrapper, src]);

    //画像サイズ未指定時のコンポーネント
    if (!width || !height) {
        return (
            <div ref={wrapper} className={className} style={{ position: 'relative', width: wrapperSize.width, height: wrapperSize.height }}>
                <ImageNext src={src} alt={alt} fill sizes={"100vw;"} style={{ objectFit: "contain" }} quality={95} priority />
            </div>
        );
    }
    // 画像サイズ指定時のコンポーネント;
    return (<div className={className}><ImageNext src={src} alt={alt} width={width} height={height} /></div>);
};

大雑把に説明すると、

  1. useStateでラッパーのサイズを初期化
  2. useRefを使ってラッパーへの参照を作成
  3. useEffectフック内でラッパーのサイズを取得
  4. 両方のサイズが確定している場合、適用して終了
  5. JSのImageオブジェクトで画像を読み込み、元画像のサイズを取得
  6. 両方のサイズが未確定の場合、元画像のサイズをラッパーコンポーネントに適用して終了
  7. 片方のサイズが確定している場合、もう片方を算出
  8. 算出したサイズをラッパーコンポーネントに適用する

という流れになっています。

使いどころ

どうしても画像要素のサイズ指定をしたくない、かつ画像のアスペクト比を維持したいとき

まとめ

画像要素のサイズ指定ができるならそれに越したことはないので、極力画像サイズに依存しない実装を心掛けたほうがよさそうです。
現状のNext.js環境では、自動判定に頼るのは茨の道です。

Discussion