0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Reactで並列処理?並行レンダリング

Posted at

はじめに

React 18 の useTransition + Suspense を使うことでReactの並列レンダリング(concurrent rendering)機能を活用し、ユーザー体験を損なわないスムーズなUI更新を実現する機能を試すことのできるコードを紹介します。

ポイント: startTransition を使った並列処理

/*
 * useTransition() で「重い処理は後でやる」宣言ができる。
 * startTransition(() => {...}) で囲まれた処理は「低優先度」で実行される。
 * → ユーザーのクリックなどの高優先度の処理を先に行う。
 */
 const [isPending, startTransition] = useTransition();

 function updateFilter(newFilter){
  // UIの状態は即座に変える(見た目が速い)
  setFilter(newFilter);

  // 重いデータ取得は後から、しかも遅延してレンダリング
  startTransition(() => {
    fetchData(newFilter);
  });
}
/*
 * フィルターの変更は即時 (setFilter)
 * データ取得は startTransition() の中 → UIの応答性を維持
 */

以下のようなユースケースで活躍しそうです。
・フィルター切り替え、検索結果表示、タブ切替など、操作直後にUIの反応が求められる処理
重たいデータフェッチや描画 を「非同期にしたいけれど、入力はすぐに反映したい 」場面
・React 18以降を使っている環境

サンプル

App.tsx
import React, { useState, useTransition, Suspense } from "react";

// フェイクAPI:フィルターに応じたデータ取得
function fetchData(filter: FilterType): Promise<string[]> {
  return new Promise((resolve) => {
    setTimeout(() => {
      const data =
        filter === "favorites"
          ? ["りんご", "バナナ"]
          : ["りんご", "バナナ", "みかん", "ぶどう"];
      resolve(data);
    }, 1000);
  });
}

// 型定義
type FilterType = "all" | "favorites";

// サスペンス対応のラッパー
function createResource<T>(promise: Promise<T>) {
  let status: "pending" | "success" | "error" = "pending";
  let result: T;
  let error: any;

  const suspender = promise.then(
    (res) => {
      status = "success";
      result = res;
    },
    (err) => {
      status = "error";
      error = err;
    }
  );

  return {
    read(): T {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw error;
      } else {
        return result!;
      }
    },
  };
}

// グローバルリソース
let resource = createResource(fetchData("all"));

// フィルター更新に応じてリソースを再生成
function loadResource(filter: FilterType) {
  resource = createResource(fetchData(filter));
}

// 結果表示コンポーネント
function Results({ filter }: { filter: FilterType }) {
  const items = resource.read();
  return (
    <ul>
      {items.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

// メインコンポーネント
export default function App() {
  const [filter, setFilter] = useState<FilterType>("all");
  const [isPending, startTransition] = useTransition();

  function updateFilter(newFilter: FilterType) {
    setFilter(newFilter);
    startTransition(() => {
      loadResource(newFilter);
    });
  }

  return (
    <div className="dashboard">
      <nav className="filter-bar">
        <button
          className={filter === "all" ? "active" : ""}
          onClick={() => updateFilter("all")}
        >
          すべて
        </button>
        <button
          className={filter === "favorites" ? "active" : ""}
          onClick={() => updateFilter("favorites")}
        >
          お気に入り
        </button>
        {isPending && <span className="loading-indicator">更新中...</span>}
      </nav>

      <div className="content">
        <Suspense fallback={<div className="skeleton-loader">読み込み中...</div>}>
          <Results filter={filter} />
        </Suspense>
      </div>
    </div>
  );
}

スタイル(お好みで)

App.css
.dashboard {
  font-family: sans-serif;
  padding: 20px;
}
.filter-bar {
  margin-bottom: 10px;
}
button {
  margin-right: 10px;
}
.active {
  font-weight: bold;
}
.loading-indicator {
  margin-left: 10px;
  color: blue;
}
.skeleton-loader {
  opacity: 0.6;
}

実行方法(Vite)

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?