🖼️
Next.js next/imageのheightとwidthを自動判定する
はじめに
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>);
};
大雑把に説明すると、
- useStateでラッパーのサイズを初期化
- useRefを使ってラッパーへの参照を作成
- useEffectフック内でラッパーのサイズを取得
- 両方のサイズが確定している場合、適用して終了
- JSのImageオブジェクトで画像を読み込み、元画像のサイズを取得
- 両方のサイズが未確定の場合、元画像のサイズをラッパーコンポーネントに適用して終了
- 片方のサイズが確定している場合、もう片方を算出
- 算出したサイズをラッパーコンポーネントに適用する
という流れになっています。
使いどころ
どうしても画像要素のサイズ指定をしたくない、かつ画像のアスペクト比を維持したいとき
まとめ
画像要素のサイズ指定ができるならそれに越したことはないので、極力画像サイズに依存しない実装を心掛けたほうがよさそうです。
現状のNext.js環境では、自動判定に頼るのは茨の道です。
Discussion