【実務・中級編】 useTransitionと副作用の優先順位付け – React実践ガイド

Reactの「重い処理」でUIを殺さない:useTransitionによる副作用の優先順位付け戦略

やあ。現場でコードを書いていると、一度は必ず壁にぶつかるはずだ。「検索フォームに入力した瞬間に、候補一覧のレンダリングが重くてタイピングがカクつく」というあの現象。

Reactはデフォルトで「すべての状態更新は平等に重要である」という前提で動いている。しかし、現実のアプリケーションでは「今すぐ反映すべきこと(入力値の更新)」と「裏で計算してもいいこと(重いフィルタリングやAPIリクエスト)」の優先順位は明確に違うんだ。

今日は、React 18以降の必須教養である `useTransition` を使って、UIの「体感速度」を劇的に改善するプロのテクニックを解説しよう。

—

ブラウザの裏側で起きている「ブロッキング」の正体

ブラウザのメインスレッドは、JavaScriptの実行とUIの描画を同じ場所で取り合っている。`setState` を呼ぶと、Reactはコンポーネントを再レンダリングし、その結果をDOMに反映しようとする。もしそのレンダリングや付随する副作用(`useEffect`内の重い処理など)に時間がかかると、ブラウザは次のフレームを描画できず、ユーザーには「画面がフリーズした」ように見える。

`useTransition` は、この「Reactの更新処理に優先順位のタグ付けをする」ためのフックだ。これを使うと、Reactに対して「この状態更新は重要度が低いから、ユーザーの入力操作を阻害しないように、裏でこっそりやってくれ」と指示を出せるようになる。

—

実践:重いフィルタリング処理を非同期にする

以下のコードを見てほしい。よくある「検索窓に入力するたびにリストを絞り込む」コンポーネントだ。これを `useTransition` なしで書くと、入力するたびにメインスレッドがロックされ、タイピングのレスポンスが悪化する。

import React, { useState, useTransition, useMemo } from ‘react’;

const SearchList = ({ items }) => {
const [query, setQuery] = useState(”);
const [deferredQuery, setDeferredQuery] = useState(”);

// startTransition: 状態更新を「低優先度」としてマークする関数
const [isPending, startTransition] = useTransition();

const handleInputChange = (e) => {
const value = e.target.value;
// 1. 入力値は即座に反映(高優先度)
setQuery(value);

// 2. フィルタリング処理は低優先度としてマーク(非ブロッキング)
startTransition(() => {
setDeferredQuery(value);
});
};

// 低優先度の状態を元に重い処理を実行する
const filteredList = useMemo(() => {
return items.filter(item => item.includes(deferredQuery));
}, [deferredQuery, items]);

return (


{/ 処理中はpending状態を表示してユーザーにフィードバックを返す /}
{isPending &&

検索中…

}

    {filteredList.map((item, i) =>

  • {item}
  • )}

);
};

ここがプロのポイント

1. 入力状態と反映状態の分離: `query`(入力用)と `deferredQuery`(描画用)を分けることで、Reactに「どこを急いで描画すべきか」を明確に伝えている。
2. `isPending` の活用: ユーザーは「何も起きない時間」が一番不安だ。`isPending` を使ってローディング状態をUIに反映させるのは、UXデザインにおける鉄則だ。
3. `useMemo` との併用: `deferredQuery` が変わったときだけ重いフィルタリング計算が走るように制御している。これにより、無駄な再計算を徹底的に排除できる。

—

副作用(useEffect)との付き合い方

ここで注意が必要だ。`useTransition` で包んだ `setState` が走ったとき、その後に実行される `useEffect` も、基本的には「非優先」として扱われるようになる。

もし「フィルタリング結果に基づいて、外部のAPIを叩く」といった副作用がある場合、`useEffect` 内で `useTransition` の状態を利用すると、通信のタイミングが遅れる可能性がある。

シニアからのアドバイス:
`useTransition` はあくまで「レンダリングの優先順位」を最適化するものだと割り切ること。もし本当に通信などの副作用の優先順位を制御したいなら、`useDeferredValue` を活用して値を遅延させ、その値の変化を `useEffect` の依存配列で監視するのが最もクリーンだ。

// useDeferredValueを使ったより宣言的なアプローチ
const deferredQuery = useDeferredValue(query);

useEffect(() => {
// deferredQueryが変わった時だけ重いAPIリクエストを投げる
// これならUIの入力操作を阻害せずに済む
fetchData(deferredQuery);
}, [deferredQuery]);

—

まとめ:賢いエンジニアは「全部をやらない」

Reactのパフォーマンスチューニングにおいて、一番の敵は「全てを完璧に同期的にやろうとすること」だ。

  • ユーザーの入力は最速で反映する。
  • 重い計算や副作用は、メインスレッドに余裕があるタイミングまで待たせる。
  • その「余裕」をReactに判断させるのが `useTransition` の真骨頂だ。

このテクニックは、ダッシュボードのフィルタリングや、大量のデータを含むフォームなど、現場で「なんとなく重い」と感じる箇所で劇的な効果を発揮する。まずは、手元のプロジェクトの「重い入力処理」を一つだけ、この方法で書き換えてみてほしい。その瞬間に、アプリの挙動が指先に吸い付くような感触に変わるはずだ。

技術は常に進化している。Reactのレンダリングモデルを理解し、ブラウザの限界を味方につける。これこそが、中級から一歩先へ進むための第一歩だ。また何かあればいつでも聞いてくれ。

コメント

タイトルとURLをコピーしました