レンダリングに時間がかかるページを  useTransition を使ってユーザーの体験を向上させる

レンダリングに時間がかかるページを  useTransition を使ってユーザーの体験を向上させる

アプリケーションを開発していると、どうしてもレンダリングに時間がかかるページが出てくることがあります。何も対策していないとレンダリングが完了するまで、ユーザー操作がブロックされユーザー体験が悪くなってしまいます。 useTransitionを使用することで、ユーザー体験を向上させることができます!
Clock Icon2024.10.07

環境

"react": "^18.3.1",
"vite": "^5.4.1",
"react-router-dom": "^6.26.2",
"tailwindcss": "^3.4.13",

useTransitionとは

useTransitionについて簡単に説明します。

const [isPending, startTransition] = useTransition();

useTransitionは UI をブロックせずに、state を更新する為の React フックになります。
2 つの要素を含む配列を返し、
1 つ目の要素はisPendingで、state が更新中かどうかを示します
2 つ目の要素はstartTransitionで、state の更新を開始する関数です。

この説明だけだと??? となると思うので、
実際にサンプルを作成しながら理解していきましょう!

完成イメージ

まずはuseTransitionを使用した場合と使用しない場合の比較を見てみましょう。
サイドメニューに「Home」「重いページ」「軽いページ」の 3 つがあります。
useTransitionを使用せずに、重いページをクリックすると、レンダリングが完了するまでユーザー操作がブロックされていることが分かります。
使用した場合、ユーザー操作がブロックされず、hover エフェクトも残っていることが分かると思います!

notUseTransition.gif
使用しない場合

useTransition.gif
使用した場合

環境構築

https://dev.classmethod.jp/articles/tailwind-auto-shaping/

上記を参考にReact + Vite + Tailwind の環境構築を行ってください

  • 今回はページ遷移を行うため、react-router-domも導入します。
npm install react-router-dom

実装

useTransition を使用しない場合

まずはuseTransitionを使用せずに作成していきます。
ディレクトリ構成は以下になっています。

src/
├── components/
│   └── Layout.tsx
├── pages/
│   ├── Home.tsx
│   ├── Heavy.tsx
│   └── Light.tsx
└── main.tsx

まずは今回の肝であるHeavy.tsx(重いページ)を作成します。
ページのレンダリングに時間がかかるように、重い計算を行う関数heavyComputationを作成し、useEffectを使用して初回レンダリング時に実行します。

pages/Heavy.tsx
import { useEffect, useState } from "react";

/** 重い計算を行う関数 */
const heavyComputation = (count: number): string[] => {
  const result: string[] = [];
  for (let i = 0; i < count; i++) {
    const complexValue = Math.sin(i) * Math.cos(i) * Math.tan(i);
    result.push(`item-${i}: ${complexValue.toFixed(10)}`);
  }
  return result;
};

const HeavyPage = () => {
  const [items, setItems] = useState<string[]>([]);

  useEffect(() => {
    setItems(heavyComputation(50000));
  }, []);

  return (
    <div className="p-4">
      <ul>
        {items.map((item) => (
          <li key={item} className="mb-1">
            {item}
          </li>
        ))}
      </ul>
    </div>
  );
};
export default HeavyPage;

そのほかのページやレイアウト、ルーティング設定を作成してきます。

その他のページ
pages/Home.tsx
function Home() {
  return <div>Home</div>;
}
export default Home;
pages/Light.tsx
const LightPage = () => {
  return <div>LightPage</div>;
};
export default LightPage;
components/Layout
import { useCallback } from "react";
import { Link, Outlet, useLocation } from "react-router-dom";

const SideBarItem = [
  {
    id: 1,
    name: "Home",
    path: "/",
  },
  {
    id: 2,
    name: "重いページ",
    path: "/heavy",
  },
  {
    id: 3,
    name: "軽いページ",
    path: "/light",
  },
];

export const Layout = () => {
  const location = useLocation();
  const isActiveLink = useCallback(
    (itemPath: string) => {
      return location.pathname === itemPath;
    },
    [location.pathname]
  );

  return (
    <div className="flex h-lvh w-lvw flex-row bg-gray-50">
      <div className="w-48 bg-blue-300">
        {SideBarItem.map((item) => (
          <Link
            key={item.id}
            to={item.path}
            className={`block p-2 hover:bg-blue-200 ${isActiveLink(item.path) ? "bg-blue-500 text-white" : ""}`}
          >
            {item.name}
          </Link>
        ))}
      </div>
      <div className="w-full overflow-y-auto bg-white p-8">{<Outlet />}</div>
    </div>
  );
};
main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import "./index.css";
import { Layout } from "./components/Layout.tsx";
import Home from "./pages/Home.tsx";
import HeavyPage from "./pages/Heavy.tsx";
import LightPage from "./pages/Light.tsx";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <Router>
      <Routes>
        <Route element={<Layout />}>
          <Route path="/" element={<Home />} />
          <Route path="/heavy" element={<HeavyPage />} />
          <Route path="/light" element={<LightPage />} />
        </Route>
      </Routes>
    </Router>
  </StrictMode>
);

これでuseTransitionを使用しない場合の実装は完了です。
画面を表示して、重いページをクリックすると、ユーザー操作がブロックされることが確認できると思います。

notUseTransition.gif

useTransition を使用する場合

現在のコードにuseTransitionを導入していきます。
変更はとても簡単です!

pages/Heavy.tsx
+import { useEffect, useState, useTransition } from "react";

 /** 重い計算を行う関数 */
 const heavyComputation = (count: number): string[] => {
   const result: string[] = [];
   for (let i = 0; i < count; i++) {
     const complexValue = Math.sin(i) * Math.cos(i) * Math.tan(i);
     result.push(`item-${i}: ${complexValue.toFixed(10)}`);
   }
   return result;
 };

 const HeavyPage = () => {
+  const [isPending, startTransition] = useTransition(); // 追加
   const [items, setItems] = useState<string[]>([]);

   useEffect(() => { // startTransition でラップする
+     startTransition(() => {
       setItems(heavyComputation(50000));
+     });
   }, []);

+  // stateが更新中の場合は、ローディングを表示
+  if (isPending) {
+    return <div>loading...</div>;
+  }
   return (
     <div className="p-4">
       <ul>
         {items.map((item) => (
           <li key={item} className="mb-1">
             {item}
           </li>
         ))}
       </ul>
     </div>
   );
 };
 export default HeavyPage;

これでuseTransitionを使用した場合の実装は完了です。
useTransitionを使用することで、ユーザー操作がブロックされず、かつローディングを表示することができます。

useTransition.gif

まとめ

useTransitionを使用することで、簡単にユーザー体験を向上させることができます!
ぜひ、実際に使用してみてください!

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.