logo

SWRでUI flickeringを対策する

2023-03-21
a year ago

開発環境

  • react 18.2.0
  • swr 2.0.0

本題

フロントエンド開発していますか?

今回はよく出くわすUI flickeringSWRを利用した対策について紹介したいと思います。

UI flickeringとは

描画中のアイテムが一瞬消えて、データの取得、stateの更新が終了したときに再度表示される、あの「画面のチラつき」のことを指します。

特に何も意識せずに実装していると大体発生します。

気にならない箇所も中にはあるかもしれませんが、これをこのまま放置しているとUI/UX的にはかなり良くないですよね。

問題の出る画面デモ

  • dummyJSONを利用してデータを取得しています
  • テキストフィールドを画面上部に設置し、入力値に応じてキー(URL)にあたる箇所を動的に変更できるようにしてリアルタイムの検索を実現しています。SWRで管理するキーを変更することになるので、入力のたびにローディングが発生します。
import { useState } from "react";
import useSWR from "swr";

const fetcher = (url: string) =>
  Promise.all([
    fetch(url),
    new Promise((res) => setTimeout(res, 600))
  ]).then(([res]) => res.json());

export default function App() {
  const [search, setSearch] = useState("iPhone");
  const { data, error, isLoading } = useSWR(
    `https://dummyjson.com/products/search?q=${search}`,
    fetcher
  );

  if (error) return "An error has occurred.";
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
      <div>
        <input
          type="text"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder="Search Products..."
          autoFocus
        />
      </div>
      {isLoading && <div>Loading...</div>}
      {data?.products.map((item) => (
        <div key={item.id}>
          <img height={300} width={300} src={item.thumbnail} alt={item.title} />
          <h3>{item.title}</h3>
          <p>{item.description}</p>
        </div>
      ))}
    </div>
  );
}

今この画面では入力をするたびに下記のローディング部分が描画されます。

{isLoading && <div>Loading...</div>}

そして、この状態ではdataが取得できておらず、mapしているデータは描画できずにUI flickeringが発生します。

対策

結論、keepPreviousDataを利用します。

そして変更するのは一箇所のみで、useSWRのオプションを追加するだけです。

const {
    data,
    error,
    isLoading
  } = useSWR(`https://dummyjson.com/products/search?q=${search}`, fetcher, {
    keepPreviousData: true // 追加
  });

keepPreviousDataを有効にすると、SWRで管理している第一引数のキーを変更して新しいキーのデータが再度ロードされても、以前のデータを取得できます。

要するに、新しいデータを取得できるまでは古いデータを返してくれるので、なめらかに再描画することができます。

注意点としては、isLoading = true のときの描画は気をつける必要があります。

例えば、今回の実装だとJSXのところで以下のように画面全体に対してローディング時の描画を切り替えるようにすると 何も描画されない=画面がチラつく ということになるので、dataの部分を表示できるような画面設計にする必要があります。

if (error) return "An error has occurred.";

if (isLoading) return <></>; // 追加

return (
  <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
    <div>
      <input
        type="text"
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        placeholder="Search Products..."
        autoFocus
      />
    </div>
    {isLoading && <div>Loading...</div>}
    {data?.products.map((item) => (
      <div key={item.id}>
        <img height={300} width={300} src={item.thumbnail} alt={item.title} />
        <h3>{item.title}</h3>
        <p>{item.description}</p>
      </div>
    ))}
  </div>
);

おまけ

YoutubeやTwitterなどで利用されていますが、実際に表示したいアイテムと同じ幅・高さを持つスケルトンを用意し、ローディング状態のときに描画させる方法も有効です。

スピナーを利用するよりもUI flickeringの度合いは軽減されます。

ただ、意味のあるアイテムをできる限り描画させ続けることがUI/UXの向上に貢献すると思いますので、こちら単体ではなく、色々な手法と組み合わせて利用していくのが良いですね。

さいごに

個人的には一覧系の画面では絞り込み検索やソート、ページネーションなどなど利用シーンが多いのかなと思っているので、参考にしていただけると嬉しいです。

参照