2
1

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】State更新時のレンダリング挙動から学ぶuseTransitionの有用性

Last updated at Posted at 2025-02-12

本記事の目的

個人的に何が嬉しいのかよく分からなかったuseTransition。Stateが更新された際の挙動を理解するとメリットが理解しやすかったので、本記事ではそこに触れつつuseTransitionの有用性について解説してみようと思います。

Stateとは何か

コンポーネント内における特定の状態を管理する方法として提供されているフックが「useState」です。

useStateは状態を参照する変数(例: count)と、状態を更新する関数(例: setCount)を返します。状態更新関数がよばれると、Reactは再レンダリングを実行し、コンポーネント内に最新の状態を反映します。

更新関数が呼ばれてすぐに最新の状態になるのではなく、更新関数を実行することで「状態更新のリクエスト」をしているというイメージが分かりやすいです!

const [count, setCount] = useState(0); // 状態「count」を定義。初期値は0

State更新時のレンダリング挙動

先述しましたが、ReactはState更新が行われると、再レンダリングを実行することで状態の更新を反映します。

例えば以下例のように、ユーザー操作に対する処理(例: increment)の中で複数のState更新が行われている場合には更新が行われている数分レンダリングが実行されているかというと、そうではありません。

Reactは同じ一連の処理の中で発生したStateの更新は全て「バッチ処理」としてまとめてレンダリングおよび反映するという挙動をとります。

つまり以下例の場合、ユーザー操作(onClick)に対する処理(例: increment)の中で、2つのState更新(例: setCountとsetAnotherCount)を行なっていますが、その更新を反映するための再レンダリングは一度のみしか実行されません。(つまり、「Rendering...」が一度のみしか出力されないということです!)

export const RenderTest: React.FC = () => {
  const [count, setCount] = useState<number>(0);
  const [anotherCount, setAnotherCount] = useState<number>(0);

  const increment = () => {
    setCount((prev) => prev + 1);
    setAnotherCount((prev) => prev + 1);
  };

  console.log("Rendering...");

  return (
    <>
      <p>Count: {count}</p>
      <p>Another count: {anotherCount}</p>
      <button onClick={increment}>+1</button>
    </>
  );
};

つまり...

例えば以下例のように、シンプルな文字列を保持するStateと、大量のリストを保持するようなStateの2つを定義していたとします。

ユーザー操作に対する一連の処理(例: handleChange)の中で、この2つのState更新を一緒に行う場合はどうでしょう。先述した通り一連の処理の中で行われるState更新および反映は全てバッチ処理としてまとめられる為、レンダリングは一度のみ行われます。

大量のリストはレンダリングコストも高い為、itemsのレンダリングに時間がかかってしまう影響で、本来軽量なはずのtextのレンダリングにも時間がかかってしまうのです!(以下を実際に動かしてみると、入力に対する画面の反応が遅くなります)

export const RenderTest: React.FC = () => {
  const [text, setText] = useState<string>("");
  const [items, setItems] = useState<string[]>([]);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value); // 軽いState更新
    setItems(Array(50000).fill(e.target.value)); // 重いState更新
  };

  console.log("Rendering...");

  return (
    <>
      <input value={text} onChange={handleChange} />
      <p>Text: {text}</p>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </>
  );
};

ここで便利なのが、useTransition

useTransitionは、特定のState更新を、優先度が高いバッチ処理から外すことができます

ここで言っている、「優先度の高いバッチ処理」と言っているのは、通常のState更新時に行われるバッチ処理のことを言っています。なので先例で紹介したものも全て「優先度の高いバッチ処理」として処理されているものです。

しかし、useTransitionを用いることで、「優先度の低いバッチ処理」というものも定義できるようになります。「優先度の低いバッチ処理」は、「優先度の高いバッチ処理」が完了した後に実行されます

useTransitionは、「トランジションが保留中であるかどうかを示すisPendingフラグ」と、「更新をトランジションとしてマークするためのstartTransition関数」の2つを返します。

startTransitionのコールバック内で書かれた処理が、トランジションとしてマークされて「優先度の低いバッチ処理」の対象として扱われます。

const [isPending, startTransition] = useTransition();

startTransition(() => {
  // 「優先度の低いバッチ処理」に含めるState更新
});

以下の例では、ユーザー操作に対する処理(例: increment)内で行われる複数のState更新うち一方(例: setAnotherCount)を、startTransitionに含めることで「優先度の低いバッチ処理」としています。

例のsetCountは、通常のバッチ処理(優先度の高いバッチ処理)の対象となる為、1回目の再レンダリングで画面に反映されますが、例のsetAnotherCountは優先度の低いバッチ処理の対象となる為、2回目の再レンダリングで画面に反映されます。(2回レンダリングが実行されているので、ボタンをクリックすると「Rendering...」が2回出力されます。)

export const RenderTest: React.FC = () => {
  const [count, setCount] = useState<number>(0);
  const [anotherCount, setAnotherCount] = useState<number>(0);

  const [isPending, startTransition] = useTransition();

  const increment = () => {
    setCount((prev) => prev + 1);
    startTransition(() => {
      setAnotherCount((prev) => prev + 1);
    });
  };

  console.log("Rendering...");

  return (
    <>
      <p>Count: {count}</p>
      <p>Another count: {anotherCount}</p>
      <button onClick={increment}>+1</button>
    </>
  );
};

これを利用することで、先述した「一連の処理の中で重たいState更新を行った場合に軽量なState更新の反映も遅れてしまう」という問題を解決することができます。

簡単です。重たいState更新をstartTransitionに含めて「優先度が低いバッチ処理」に含めれば完了です。

こうすることで、軽量のState更新(例: setText)は通常のバッチ処理(優先度が高いバッチ処理)に含まれる為すぐに画面に反映され、重たいState更新(例: setItems)は「優先度が低いバッチ処理」として通常のバッチ処理が完了した後に反映されるので、重たいState更新反映が軽量なState更新反映まで遅くする問題が解決します。

さらに、トランジションが保留中であるかどうかを示すisPendingフラグを使用することで、「優先度が低いバッチ処理」が進行中であることを示すインジケーターなどを実装可能です。

export const RenderTest: React.FC = () => {
  const [text, setText] = useState<string>("");
  const [items, setItems] = useState<number[]>([]);

  const [isPending, startTransition] = useTransition();

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value); // 軽いState更新
    startTransition(() => {
      setItems(Array(50000).fill(e.target.value)); // 重いState更新
    });
  };

  console.log("Rendering...");

  return (
    <>
      <input value={text} onChange={handleChange} />
      <p>Text: {text}</p>
      {isPending ? (
        <p>Searching with {text}...</p>
      ) : (
        <ul>
          {items.map((item, index) => (
            <li key={index}>{item}</li>
          ))}
        </ul>
      )}
    </>
  );
};

useTransition、便利です!🤩

まとめ

  • Reactはユーザー操作に対する一連の処理の中で行われるStateの更新は全て「バッチ処理」として一回の再レンダリングで画面に反映するという挙動がある。しかし、重たいState更新が含まれる場合、軽量なState更新の画面反映まで遅くなってしまうという問題がある
  • useTransitionを用いることで、特定のState更新を通常のバッチ処理とは別の「優先度が低いバッチ処理」とに含めることができる。これを利用することで、重たいState更新を通常のバッチ処理の対象から外すことができるので、上記の問題を解決することができる
  • 更に「優先度が低いバッチ処理」が進行中であることを知らせるフラグisPendingも提供されているので、それを利用することでインジケーターなども実装できるので便利
2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?