🛸

React18の新機能(Transition)を使用したスムーズなトランジション効果を確認

2023/09/18に公開

はじめに

今回は React18 の新機能Transitionについて確認していきたいと思います。
Automatic Batchig についてはこちらの記事をご覧ください。
https://zenn.dev/rh820/articles/3403e2b1533f6c

動作確認用リポジトリ

https://github.com/dahoho/react-18-demo

Transition とは?

トランジション(transition; 段階的推移)とは React における新たな概念であり、緊急性の高い更新 (urgent update) と高くない更新 (non-urgent update) を区別するためのものです。
緊急性の高い更新とはタイプ、クリック、プレスといったユーザ操作を直接反映するものです。
トランジションによる更新は UI をある画面から別の画面に段階的に遷移させるものです。
タイプ、クリック、プレスのような緊急性の高い更新は、物理的な物体の挙動に関する我々の直観に反しないよう、即座に反応する必要があり、そうでないと「おかしい」と認識されてしまいます。一方でトランジション内では、ユーザは画面上であらゆる中間の値が見えることを期待していません。

引用元:https://ja.react.dev/blog/2022/03/29/react-v18#new-feature-transitions

トランジションは優先度が高い変更と低い変更を区別して、優先度の高い変更を優先的に行う機能です。
例えば、ボタンをクリックしたときに、以下二つの変更を行う場合を考えてみます。

  • ボタンの色を変更する(高優先度)
  • メッセージを表示する(低優先度)

この場合、ボタンの色を変更することは、ユーザーの操作を直接反映する部分なので、優先度は高いと考えます。
一方で、メッセージを表示する変更は、ユーザーの操作に直接関わる部分ではないため、優先度は低いとします。
トランジション機能を使用することで、高優先度の変更である「ボタンの色を変更する」変更が、低優先度の変更である「メッセージを表示する」変更よりも優先的に実行されます。

Transition を使用したレンダリングコストの高いコンポーネントの改善と動作確認

10000 件のデータ表示をフィルタリングしている、レンダリングコストが高いコンポーネントを作成して動作確認をしてみます。

TransitionTest.tsx
import { useState } from "react";
import MemberButton from "./MemberButton";

type Task = {
  id: number;
  title: string;
  name: string;
  description: string;
};

const member = {
  a: "A",
  b: "B",
  c: "C",
};

const createTaskArray = (): Task[] => {
  return Array(10000)
    .fill("")
    .map((_, index) => {
      const addIndex = index + 1;
      return {
        id: addIndex,
        title: `title${addIndex}`,
        description: `description${addIndex}`,
        name:
          addIndex % 3 === 0
            ? member.a
            : addIndex % 2 === 0
            ? member.b
            : member.c,
      };
    });
};

const tasksArray = createTaskArray();

const filteringMember = (member: string) => {
  if (member === "") return tasksArray;
  return tasksArray.filter((task) => task.name === member);
};

const TransitionTest = () => {
  const [selectMember, setSelectMember] = useState<string>("");
  const [tasks, setTasks] = useState<Task[]>(tasksArray);

  const clickMember = (member: string) => {
    console.log("クリックされました!!");
    setSelectMember(member);

    setTasks(filteringMember(member));
  };

  return (
    <>
      <p className="text-3xl text-center mt-20">TransitionTest</p>
      <div className="flex justify-center gap-5 mt-8">
        <MemberButton
          onClick={clickMember}
          isActive={selectMember === member.a}
        >
          {member.a}
        </MemberButton>
        <MemberButton
          onClick={clickMember}
          isActive={selectMember === member.b}
        >
          {member.b}
        </MemberButton>
        <MemberButton
          onClick={clickMember}
          isActive={selectMember === member.c}
        >
          {member.c}
        </MemberButton>
      </div>
      <div className="text-center mt-8">
        <button
          className="bg-gray-600 text-white py-2 px-4 m-auto inline-block"
          onClick={() => clickMember("")}
        >
          リセット
        </button>
      </div>

      <ul className={`grid gap-4 mt-8`}>
        {tasks?.map((task) => (
          <li key={task.id} className="bg-gray-300 w-80 m-auto p-3">
            <p>タイトル: {task.title}</p>
            <p>説明: {task.description}</p>
            <p>名前: {task.name}</p>
          </li>
        ))}
      </ul>
    </>
  );
};

export default TransitionTest;

改善前

タブをクリックしたタイミングでコンソールが出力されていますが、背景色が変わるまでに時間がかかっていることが分かります。
操作している側からしたら使いづらいですね・・

useTransition を使用した改善

useTransition を import して、配列の分割代入でisPendingstartTransitionを定義します。
優先度が低く、Transition をさせたい set 関数をstartTransitionの中に入れます。
またisPendingを使用することで startTransision 内で set 関数の state が更新中かどうかのフラグを boolean として返してくれます。
これを利用して、更新中は「更新中です 😌」というテキストを表示させてみることとします。
https://react.dev/reference/react/useTransition

TransitionTest.tsx
+ import { useState, useTransition } from "react";
import MemberButton from "./MemberButton";

type Task = {
  id: number;
  title: string;
  name: string;
  description: string;
};

const member = {
  a: "A",
  b: "B",
  c: "C",
};

const createTaskArray = (): Task[] => {
  return Array(10000)
    .fill("")
    .map((_, index) => {
      const addIndex = index + 1;
      return {
        id: addIndex,
        title: `title${addIndex}`,
        description: `description${addIndex}`,
        name:
          addIndex % 3 === 0
            ? member.a
            : addIndex % 2 === 0
            ? member.b
            : member.c,
      };
    });
};

const tasksArray = createTaskArray();

const filteringMember = (member: string) => {
  if (member === "") return tasksArray;
  return tasksArray.filter((task) => task.name === member);
};

const TransitionTest = () => {
+  const [isPending, startTransition] = useTransition();
  const [selectMember, setSelectMember] = useState<string>("");
  const [tasks, setTasks] = useState<Task[]>(tasksArray);

  const clickMember = (member: string) => {
    console.log("クリックされました!!");
    setSelectMember(member);
+    startTransition(() => {
+      setTasks(filteringMember(member));
+    });
  };

  return (
    <>
      <p className="text-3xl text-center mt-20">TransitionTest</p>
      <div className="flex justify-center gap-5 mt-8">
        <MemberButton
          onClick={clickMember}
          isActive={selectMember === member.a}
        >
          {member.a}
        </MemberButton>
        <MemberButton
          onClick={clickMember}
          isActive={selectMember === member.b}
        >
          {member.b}
        </MemberButton>
        <MemberButton
          onClick={clickMember}
          isActive={selectMember === member.c}
        >
          {member.c}
        </MemberButton>
      </div>
      <div className="text-center mt-8">
        <button
          className="bg-gray-600 text-white py-2 px-4 m-auto inline-block"
          onClick={() => clickMember("")}
        >
          リセット
        </button>
      </div>
+      {isPending ? (
+        <p className="text-center mt-12 text-xl">更新中です😌</p>
+      ) : (
+        <ul className={`grid gap-4 mt-8`}>
+          {tasks?.map((task) => (
+            <li key={task.id} className="bg-gray-300 w-80 m-auto p-3">
+              <p>タイトル: {task.title}</p>
+              <p>説明: {task.description}</p>
+              <p>名前: {task.name}</p>
+            </li>
+          ))}
+        </ul>
+      )}
    </>
  );
};

export default TransitionTest;

改善後

タブをクリックしてから、タグの背景色が変わるまでの時間が短くなり、とても使いやすくなりました!
また優先度の低い、絞り込み結果側の表示は少し遅れていることが分かります。

まとめ

今回は React18 の新機能Transitionの動作確認をしてみました。
実際にコードを動かしてみることで、優先度の高い変更と低い変更を区別して、優先度の高い変更を優先的に行うことができることが分かりました。
今後は Suspense などの機能についても確認していきたいです。

参考文献

https://ja.legacy.reactjs.org/blog/2022/03/29/react-v18.html
https://www.udemy.com/course/react_v18/

GitHubで編集を提案

Discussion