導入
先日 Introducing MUI Core v5.0 で発表があり、Material-UIのバージョンが更新されました。
前から気になっていたスケルトンビューが、実験室から昇格してきたので、製品に実装するために動作確認を行いました。
ローディング画面で表示されるUIです。従来のローディングスピナーやプログレスバーに加えて、ユーザーの待機ストレスを軽減する目的があります。
Google による Material Desin の説明では、Placeholder-UIと呼ばれており、Material-UIでは Skeletonと呼ばれます。
動作デモ
@codesandbox
これから読み込まれるコンテンツの形を、ワイヤーフレームよりはリッチな見た目で見せてくれるものです。
公式の使い方
{
item ? (
<img
style={{
width: 210,
height: 118,
}}
alt={item.title}
src={item.src}
/>
) : (
<Skeleton variant="rectangular" width={210} height={118} />
);
}
そんな簡単じゃないんだよ
公式の説明を読んで、「3項演算子で読込完了に合わせて切り替えるだけか、簡単だぞ!」と、意気揚々とソースを書き換え終えた私は、npm start
で実行してみて気が付きます。
「あれ?画像読み込む前に読込完了扱いになってしまう!」「あれれ~?おかしいな~公式ドキュメントの通りやれば上手くいくって、新一お兄ちゃんが言ってたよ?」
この時のソースはこんな感じ
interface IImageRender {
src: string
title: string
}
const ImageRender: React.FC<IImageRender> = (props) => {props.src
? <Stack>
<Skeleton variant="rectangular" width={180} height={140} />
<Skeleton variant="text" />
</Stack>
: <>
<img src={img.src} alt="" max-width="85%" width="auto" height="140px" style={{ borderRadius: "0.2pc", display: (loading ? 'hidden' : undefined) }} />
<Typography style={{ display: (loading ? 'hidden' : undefined) }} >{props.title}</Typography>
</>}
「 props.src が true になるのは、画像読込が完了したタイミングなんやろ?」「公式ドキュメントのサンプルコードが物語っている」
残念ながらそんなことありません。
開発者ツールで見てみよう
どうやってデバッグするのかというと、
ブラウザの開発者ツールにあるパフォーマンスタブを使います。
しかし、その前に、ネットワークの速度を下げておきましょう。
そうすれば、疑似的に3G回線の再現実験を行うことができます。
物理的に下げると、そこはかとなく面倒なので設定で下げます。
Network タブ
まず、F12 キーで Chrome developer tool を開いて、Network タブに移動します。
ここに、No throttling と書いたドロップダウンリストがあります。
こちらをお好みのスピードに下げることで、目視でもある程度は動作を見れるようになります。
Performance タブ
さて、ここからはもう少し細かい時間スケールで検証できる方法を見ていきます。
F12 キーで Chrome developer tool を開いて、Performance タブに移動します。ここで Reload ボタンをクリックします。
検証したい部分の動作が終わったと判断したら stop ボタンをクリックします。
すると、スクリーンショットのサムネイル画像が時系列で並んだ行が見えます。
この中で、一度白い部分が増えている時間が見つかります。
マウスをホバーすると、画像が拡大されて、詳細を見ることができます。
読み込めてないのに表示してる
useEffectを使ってみよう
「propsのtruefalseでは読込完了の判断は出来ないっぽいぞ?」
「たしか、useEffectってDom描画の完了後に呼ばれるんだった。」
という発想から
「これでuseEffectとuseStateで切り替えてやればいいのでは?」と、またも仮説を立てます。
検証コードを書いてみます。
interface IImageRender {
src: string
title: string
}
const ImageRender: React.FC<IImageRender> = (props) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(false);
};
},[]);
return <>{loading
? <Stack>
<Skeleton variant="rectangular" width={180} height={140} />
<Skeleton variant="text" />
</Stack>
: <>
<img src={img.src} alt="" max-width="85%" width="auto" height="140px" style={{ borderRadius: "0.2pc", display: (loading ? 'hidden' : undefined) }} />
<Typography style={{ display: (loading ? 'hidden' : undefined) }} >{props.title}</Typography>
</>}
</>
}
useEffectを使ってみた結果
①読込開始
②UseEffectによる切り替わり
③画像読込完了
「いや、Skeleton 意味無いし」
useEffectの中はいつ動く?
今回の例では画像の読み込みが一番時間がかかります。そのため、画像の読み込み時間をSkeleton
の表示期間と定めます。
とりあえず、「useEffect
の実行タイミングの認識間違ってたんかな?」との仮説を立てて、公式ドキュメントを読んでみました。
useEffect に渡された関数はレンダーの結果が画面に反映された後に動作します。
「あってるんじゃないの?よくわからん。」
ということで、useEffect君のことは、いったん忘れます。さようなら。
もういっそ全部
「window.onloadってのがあったよね」
「windowの中身を全部読込終わったら表示すれば良さそう」という仮説をたて、検証してみます。
interface IImageRender {
src: string
title: string
}
const ImageRender: React.FC<IImageRender> = (props) => {
const [loading, setLoading] = useState(true);
window.onload = () => {
setLoading(false);
}
return <>{loading
? <Stack>
<Skeleton variant="rectangular" width={180} height={140} />
<Skeleton variant="text" />
</Stack>
: <>
<img src={img.src} alt="" max-width="85%" width="auto" height="140px" style={{ borderRadius: "0.2pc", display: (loading ? 'hidden' : undefined) }} />
<Typography style={{ display: (loading ? 'hidden' : undefined) }} >{props.title}</Typography>
</>}
</>
}
「あれ?window.onloadに関数上書きできてるけど、」
「<ImageRender/>
が生成されるたびに上書きしてるから、ダメやん。」
「1個だけしか動かへん」
個別に扱いたい
「<ImageRender/>
のuseStateを静的に使えたらいいけど、
そんなドキュメントあったかな?」
「imgタグ単体で扱えたらいいのに」などと考えていると、
「imgオブジェクトにonload関数あるやん」と。
「先にimgオブジェクトを生成して、読み込んでおいたらいいかも」と仮説を用意。
そうして出来上がったのがこちら。
interface IImageRender {
src: string
title: string
}
const ImageRender: React.FC<IImageRender> = (props) => {
const [loading, setLoading] = useState(true);
const img = new Image()
img.src = props.src // preload
img.onload = () => {
setLoading(false);
}
return <>{loading
? <Stack>
<Skeleton variant="rectangular" width={180} height={140} />
<Skeleton variant="text" />
</Stack>
: <>
<img src={img.src} alt="" max-width="85%" width="auto" height="140px" style={{ borderRadius: "0.2pc", display: (loading ? 'hidden' : undefined) }} />
<Typography style={{ display: (loading ? 'hidden' : undefined) }} >{props.title}</Typography>
</>}
</>
}
最終結果
①読込開始で<Skeleton/>
を表示
②画像読込完了をimg.onload
イベントで検知
検証環境
- mui 5.0.0 (旧名 material-ui)
- typescript 4.3.2
- react 17.0.2
まとめ
この記事でSkeletonを使う開発者が増え、多くのサイトでUXが向上するといいと思います。
それでは、ハッピーハッキング!
Excelsior!