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

# 導入

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

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

Google による Material Desin の説明では、[Placeholder-UI](https://material.io/design/communication/launch-screen.html#placeholder-ui)と呼ばれており、Material-UIでは [Skeleton](https://mui.com/components/skeleton/)と呼ばれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/237d621e-1d48-4b6e-e628-e476926bea96.png)

動作デモ
@[codesandbox](https://codesandbox.io/embed/skeletonchildren-material-demo-vchf7?fontsize=14&hidenavigation=1&theme=dark)

これから読み込まれるコンテンツの形を、ワイヤーフレームよりはリッチな見た目で見せてくれるものです。

# 公式の使い方

[公式の説明](https://mui.com/components/skeleton/)

```tsx
{
  item ? (
    <img
      style={{
        width: 210,
        height: 118,
      }}
      alt={item.title}
      src={item.src}
    />
  ) : (
    <Skeleton variant="rectangular" width={210} height={118} />
  );
}
```

# そんな簡単じゃないんだよ

公式の説明を読んで、「３項演算子で読込完了に合わせて切り替えるだけか、簡単だぞ！」と、意気揚々とソースを書き換え終えた私は、`npm start`で実行してみて気が付きます。

「あれ？画像読み込む前に読込完了扱いになってしまう！」「あれれ～？おかしいな～公式ドキュメントの通りやれば上手くいくって、新一お兄ちゃんが言ってたよ？」

この時のソースはこんな感じ

```tsx
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/c1d1cd05-f4fe-9c34-ec6c-6563912659f8.png)

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

## Performance タブ

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

F12 キーで Chrome developer tool を開いて、Performance タブに移動します。ここで Reload ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/c41494bd-f023-472f-da3c-8dba534ea0be.png)

すると、ページの最読み込みが始まります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/ab506420-0990-6e4f-427d-0a27baa78746.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/22219909-6f3d-64c0-c629-dad12d20fd8a.png)

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

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

![ddd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/2909769e-da89-1c3d-5c4a-4ee3dd8090a1.png)　　![ff.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/9a07ee96-88ed-7c06-58b2-d44673889a8d.png)
開発者ツールで見てみると、スケルトンを使っていません。

# useEffectを使ってみよう

「propsのtruefalseでは読込完了の判断は出来ないっぽいぞ？」
「たしか、useEffectってDom描画の完了後に呼ばれるんだった。」
という発想から
「これでuseEffectとuseStateで切り替えてやればいいのでは？」と、またも仮説を立てます。

検証コードを書いてみます。

```tsx
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/0ec0a119-4510-5076-6751-b0bfb59a195a.png)
②UseEffectによる切り替わり
![ddd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/2909769e-da89-1c3d-5c4a-4ee3dd8090a1.png)
③画像読込完了
![ff.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/9a07ee96-88ed-7c06-58b2-d44673889a8d.png)

「いや、Skeleton 意味無いし」

# useEffectの中はいつ動く？

今回の例では画像の読み込みが一番時間がかかります。そのため、画像の読み込み時間を`Skeleton`の表示期間と定めます。

とりあえず、「`useEffect`の実行タイミングの認識間違ってたんかな？」との仮説を立てて、[公式ドキュメント](https://ja.reactjs.org/docs/hooks-reference.html#useeffect)を読んでみました。

> useEffect に渡された関数はレンダーの結果が画面に反映された後に動作します。

「あってるんじゃないの？よくわからん。」
ということで、useEffect君のことは、いったん忘れます。さようなら。

# もういっそ全部

「window.onloadってのがあったよね」
「windowの中身を全部読込終わったら表示すれば良さそう」という仮説をたて、検証してみます。

```tsx
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/>`が生成されるたびに上書きしてるから、ダメやん。」
「１個だけしか動かへん」

# 個別に扱いたい

「`<ImageRender/>`のuseStateを静的に使えたらいいけど、
そんなドキュメントあったかな？」

「imgタグ単体で扱えたらいいのに」などと考えていると、

「imgオブジェクトに[onload関数](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onload)あるやん」と。
「先にimgオブジェクトを生成して、読み込んでおいたらいいかも」と仮説を用意。

そうして出来上がったのがこちら。

```tsx
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/0ec0a119-4510-5076-6751-b0bfb59a195a.png)
②画像読込完了を`img.onload`イベントで検知
![ff.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/9a07ee96-88ed-7c06-58b2-d44673889a8d.png)

# 検証環境

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

# まとめ

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

**Excelsior!**