4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

MUIv5のSkeletonで簡単にplaceholder-uiを使う方法

Last updated at Posted at 2021-10-05

導入

先日 Introducing MUI Core v5.0 で発表があり、Material-UIのバージョンが更新されました。
前から気になっていたスケルトンビューが、実験室から昇格してきたので、製品に実装するために動作確認を行いました。

ローディング画面で表示されるUIです。従来のローディングスピナーやプログレスバーに加えて、ユーザーの待機ストレスを軽減する目的があります。

Google による Material Desin の説明では、Placeholder-UIと呼ばれており、Material-UIでは Skeletonと呼ばれます。

image.png

動作デモ
@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 と書いたドロップダウンリストがあります。
image.png

こちらをお好みのスピードに下げることで、目視でもある程度は動作を見れるようになります。

Performance タブ

さて、ここからはもう少し細かい時間スケールで検証できる方法を見ていきます。

F12 キーで Chrome developer tool を開いて、Performance タブに移動します。ここで Reload ボタンをクリックします。
image.png

すると、ページの最読み込みが始まります。
image.png

検証したい部分の動作が終わったと判断したら stop ボタンをクリックします。

image.png

すると、スクリーンショットのサムネイル画像が時系列で並んだ行が見えます。
この中で、一度白い部分が増えている時間が見つかります。
マウスをホバーすると、画像が拡大されて、詳細を見ることができます。

読み込めてないのに表示してる

ddd.png  ff.png
開発者ツールで見てみると、スケルトンを使っていません。

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を使ってみた結果

①読込開始
fasdf.png
②UseEffectによる切り替わり
ddd.png
③画像読込完了
ff.png

「いや、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/>を表示
fasdf.png
②画像読込完了をimg.onloadイベントで検知
ff.png

検証環境

  • mui 5.0.0 (旧名 material-ui)
  • typescript 4.3.2
  • react 17.0.2

まとめ

この記事でSkeletonを使う開発者が増え、多くのサイトでUXが向上するといいと思います。
それでは、ハッピーハッキング!

Excelsior!

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?