Tags:
TypeScript
React
最近仕事で使ったテクニック
仕事で使ったテクニックを個人のブログに記しておかないと退職時に失ってしまうので書き写しておく。「ブラウザが webp をサポートしているかどうか判定」、「画像が読み込み終わったことを検知する」、「IntersectionObserver を使うための React hook」について。
ブラウザが webp をサポートしているかどうか
画像を表示するなら <picture>
を使えばいい話だが、CSS 側でどうしても background-image
を使わざるを得なくて、そこで webp と png を出し分けたい場合などにブラウザ側で webp サポートを判定して data 属性にセットしたりして使う。
/**
* ブラウザが webp をサポートしているかどうか
*
* @returns webp をサポートしているなら true そうでないなら false
*/
export const supportsWebp = async () => {
return new Promise<boolean>((resolve) => {
const img = new Image();
img.onload = () => {
resolve(img.width > 0);
};
img.onerror = () => {
resolve(false);
};
img.src =
'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA';
});
};
なおググってみると @supports (background-image: url('logo.webp'))
で webp 対応判定できるという情報が見つかるが、実際に試すと判定できないので鵜呑みにしてはいけない。
私は iOS Simulator で webp 対応・非対応の両バージョンの Safari で試し、上記の supportsWebp
関数が意図通りに動くことを確認した。
また img.src
を指定した後に img.onload
を書くとキャッシュから画像が読み込まれたとき上手くいかないので、img.src
は最後に書くという順番が大事である。
画像が読み込み終わったことを検知する
画像がキャッシュされている場合、<img>
に指定した onLoad
は発火しないので、imgRef.current?.complete
も見ることが必要。
import { useCallback, useEffect, useRef, useState } from 'react';
export function Page() {
const [isLoaded, setIsLoaded] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);
const onLoad = useCallback(() => {
setIsLoaded(true);
}, []);
useEffect(() => {
if (imgRef.current?.complete) {
onLoad();
}
}, [onLoad]);
return (
<img
ref={imgRef}
src={"/image/dummy.jpg"}
alt=""
width={960}
height={600}
onLoad={onLoad}
/>
);
};
IntersectionObserver を使うための React hook
ビューポートに要素が入った瞬間に何か実行したいときに使う。
import { useEffect } from 'react';
type useIntersectionObserverProps = {
/** ルート要素。nullの場合はビューポート。 */
rootRef: React.RefObject<HTMLDivElement> | null;
/** ターゲット要素 */
targetRef: React.RefObject<HTMLDivElement>;
/** 交差したときの動作 */
intersectionCallBack: () => void;
/**
* 交差率(0.0〜1.0)
* @default 0.1
*/
threshold?: number;
};
export const useIntersectionObserver = ({
rootRef,
targetRef,
intersectionCallBack,
threshold = 0.1,
}: useIntersectionObserverProps) => {
useEffect(() => {
if ((rootRef && !rootRef.current) || !targetRef.current) return;
const callback = (entries: IntersectionObserverEntry[]) => {
if (entries[0].isIntersecting) intersectionCallBack();
};
const observer = new IntersectionObserver(callback, {
root: rootRef ? rootRef.current : null,
rootMargin: '0px',
threshold,
});
const target = targetRef.current;
observer.observe(target);
return () => {
observer.unobserve(target);
};
}, [rootRef, intersectionCallBack, targetRef, threshold]);
};