10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

KDDI Engineer&Designer Advent Calendar 2022

Day 4

React18の新機能と変更点を実際に手元で動かして理解してみる(自動バッチング, トランジション機能, startTransition(), useTransition(), useDeferredValue())

Last updated at Posted at 2022-12-03

はじめに

こんにちは、都内でソフトウェアエンジニアをしているYSasagoと申します。
今回は、2022年3月に登場したReactバージョン18の新機能と変更点について実際に手元で動かして確認しようと思います。
React18の新機能は、意識して使わないと使えない機能もあるので、使い方をまとめていきたいと思います。

変更点1: アプリケーションのrootの書き方の変更

React18でアプリケーションのrootの書き方の変更がありました。
今から、create react-appでプロジェクトを作成するのであれば、React18のプロジェクトが作成されるので、特に意識しなくても大丈夫だと思います。
React17を使いたい時や、React18から17へダウングレードする時のために、変更点を備忘録としてまとめたいと思います。

React18で17の記法を使っていると下記のWarningが表示されます。

Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot

スクリーンショット 2022-11-05 12.48.19.png

React18の記法にしないとReact18の新機能使えないよー的なことが書いてあります。

React17のプロジェクト作成方法

create react-appでReactのプロジェクトを作成すると、React18のプロジェクトが作成されます。

$ yarn create react-app react18-explanation-react17 --template typescript

React18からReact17にダウングレードさせるため、reactreact-domのライブラリをver17.0.2に更新します。

$ yarn add react@17.0.2 react-dom@17.0.2

package.jsonの中身を確認すると、reactのver17.0.2が入っていることがわかります。

スクリーンショット 2022-11-05 12.11.13.png

React18でアプリケーションのルートの書き方の変更があったため、src/index.tsxを、React17で動くように修正します。

src/index.tsx
import React from "react";
/* React17の書き方 */
import ReactDOM from "react-dom";
 /* React18の書き方 */
// import ReactDOM from 'react-dom/client';
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

/* React18の書き方 */
// const root = ReactDOM.createRoot(
//   document.getElementById('root') as HTMLElement
// );
// root.render(
//   <React.StrictMode>
//     <App />
//   </React.StrictMode>
// );

/* React17の書き方 */
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

実行して、http://localhost:3000で画面が表示されればOKです。

$ yarn start

react17.png
React17のプロジェクトが作成できました。

React18のプロジェクト作成方法

create react-appでReactのプロジェクトを作成します。

$ yarn create react-app react18-explanation-react18 --template typescript

package.jsonの中身を確認すると、Reactのver18.2.0が入っていることがわかります。

スクリーンショット 2022-11-05 11.49.25.png

実行して、http://localhost:3000で画面が表示されればOKです。
プロジェクトを作成しただけなので、何も問題は起きないと思います。

$ yarn start

react17.png
React18のプロジェクトが作成できました。

変更点2: Strict モードの新たな挙動

今後のReactの標準状態でのパフォーマンスが向上のため、Strictモードのときに、すべてのコンポーネントを自動的にアンマウント・再マウントし、かつ 2回目のマウントで以前の state を復元するようになりました。
実際に手元で動かして、動作を確認しようと思います。

将来的に、React が state を保ったままで UI の一部分を追加・削除できるような機能を導入したいと考えています。例えば、ユーザがタブを切り替えて画面を離れてから戻ってきた場合に、React が以前の画面をすぐに表示できるようにしたいのです。これを可能にするため、React は同じ state を使用してツリーをアンマウント・再マウントします。

src/App.tsxに初回レンダリングのときにlogが表示されるように、useEffectを追加します。

src/App.tsx
import React, { useEffect } from "react";
import logo from "./logo.svg";
import "./App.css";

function App() {
// 初回レンダリングのときにlogを表示する
  useEffect(() => {
    console.log("useEffect");
  }, []);

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

画面を確認すると。React17の動作の時と異なり、React18では、自動的にアンマウント・再マウントするため、logが2回表示されていることがわかります。
つまり、今後React18で開発進める場合は、Reactの機能追加のために、2回レンダリングに耐える設計にしていく必要があります。

スクリーンショット 2022-11-05 13.16.43.png

index.tsxに書いてある、<React.StrictMode>を削除すれば、React17のときの挙動と同じになります。

src/index.tsx
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
import reportWebVitals from "./reportWebVitals";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
// root.render(
//   <React.StrictMode>
//     <App />
//   </React.StrictMode>
// );

root.render(<App />);

reportWebVitals();

logが1つになりました。React17時の状態の動作確認をしたいときに使うと良いかもです。
スクリーンショット 2022-11-05 13.24.01.png

新機能:自動バッチング

React17では、イベントハンドラ以外でSET関数を複数呼んだら、呼ばれるたびにレンダリングが動いてました。
React18では、イベントハンドラ以外でSET関数を呼んでも、Reactがある程度のかたまりでまとめて動作してくれるようになりました。

バッチングとは React がパフォーマンスのために複数のステート更新をグループ化して、単一の再レンダーにまとめることを指します。自動バッチング以前は、React のイベントハンドラ内での更新のみバッチ処理されていました。promise や setTimeout、ネイティブのイベントハンドラやその他あらゆるイベント内で起きる更新はデフォルトではバッチ処理されていませんでした。自動バッチングにより、これらの更新も自動でバッチ処理されるようになります

React17で動作確認

イベントハンドラ以外でSET関数を2回呼ぶコンポーネントを作成して、動作確認してみます。

src/components/AutoBatchOther.tsx
import { useState } from "react";

type Todo = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

export const AutoBatchOther = () => {
  console.log("AutoBatchOther");

  const [todos, setTodos] = useState<Todo[] | null>(null);
  const [isFinish, setIsFinish] = useState(false);

  const onClickButton = () => {
    fetch("https://jsonplaceholder.typicode.com/todos")
      .then((res) => res.json())
      .then((data) => {
        // 2つのSET関数が呼ばれている
        setTodos(data);
        setIsFinish(true);
      });
  };
  return (
    <div>
      <p>AutoBatchOther</p>
      <button onClick={onClickButton}>API実行</button>
      <p>isFinish : {isFinish ? "true" : "false"}</p>
      {todos?.map((todo: Todo) => (
        <p key={todo.id}>{todo.title}</p>
      ))}
    </div>
  );
};
src/App.tsx
import "./App.css";
import { AutoBatchOther } from "./components/AutoBatchOther";

function App() {
  return (
    <div className="App">
      <AutoBatchOther />
    </div>
  );
}

export default App;

ボタンを押すと、SET関数を2回呼んでいるため、logが2個ずつ表示されていることがわかります。
logが3個表示されているのは、初回レンダリング時に1個表示されるためです。

スクリーンショット 2022-11-05 15.17.21.png

React18で動作確認

コードは上記と同じで、React18で動作確認してみます。
ボタンを押すと、SET関数を2回呼んでいますが、logが1個ずつ表示されていることがわかります。
logが2個表示されているのは、初回レンダリング時に1個表示されるためです。
このように、React18だと、自分たちで何もしなくても、自動でバッチ処理の最適化をしてくれます。

スクリーンショット 2022-11-05 15.29.11.png

新機能: トランジション

React18では、トランジション機能が追加されました。
トランジション機能を使うと、緊急性の高い処理(画面にすぐ反映したい処理)と緊急性の低い処理を分けることができるようになります。トランジション機能を用いることでユーザビリティの向上につなげることができます。

トランジション(transition; 段階的推移)とは React における新たな概念であり、緊急性の高い更新 (urgent update) と高くない更新 (non-urgent update) を区別するためのものです。
緊急性の高い更新とはタイプ、クリック、プレスといったユーザ操作を直接反映するものです。
トランジションによる更新は UI をある画面から別の画面に段階的に遷移させるものです。

サンプルアプリの作成

トランジション機能を確認するために、1万個のダミーデータを、画面に表示するサンプルプログラムを作成しました。
ボタンを押すと1万個のダミーデータをフィルタリングする機能も入れました。

FireShot Capture 113 - React App - localhost.png

src/components/Transition.tsx
import { useState } from "react";
import { Avatar } from "./Avatar";

type Task = {
  id: number;
  title: string;
  assignee: string;
};

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

const generateDummyTasks = (): Task[] => {
  return Array(10000)
    .fill("")
    .map((_, index) => {
      return {
        id: index,
        title: `タスク${index}`,
        assignee:
          index % 3 === 0 ? member.a : index % 2 === 0 ? member.b : member.c,
      };
    });
};
const tasks = generateDummyTasks();

export const Transition = () => {
  const [selectedAssignee, setSelectedAssignee] = useState<string>("");
  const [taskList, setTaskList] = useState<Task[]>(tasks);

  const onClickAssignee = (assignee: string) => {
    setSelectedAssignee(assignee);
    setTaskList(filteringAssignee(assignee));
  };

  const filteringAssignee = (assignee: string) => {
    if (assignee === "") return tasks;
    return tasks.filter((task) => task.assignee === assignee);
  };

  return (
    <div>
      <p>Transition</p>
      <div style={{ display: "flex", justifyContent: "center", gap: "5px" }}>
        <Avatar
          isSelected={selectedAssignee === member.a}
          onClick={onClickAssignee}
        >
          {member.a}
        </Avatar>
        <Avatar
          isSelected={selectedAssignee === member.b}
          onClick={onClickAssignee}
        >
          {member.b}
        </Avatar>
        <Avatar
          isSelected={selectedAssignee === member.c}
          onClick={onClickAssignee}
        >
          {member.c}
        </Avatar>
      </div>
      <button onClick={() => onClickAssignee("")}>リセット</button>
      {taskList.map((task) => {
        return (
          <div
            key={task.id}
            style={{
              width: "300px",
              margin: "auto",
              backgroundColor: "skyblue",
            }}
          >
            <p>タイトル: {task.title}</p>
            <p>担当者: {task.assignee}</p>
          </div>
        );
      })}
    </div>
  );
};

src/components/Avatar.tsx
import { ReactNode } from "react";

type Props = {
  children: ReactNode;
  isSelected?: boolean;
  onClick: (assignee: string) => void;
};

export const Avatar = ({ children, isSelected = false, onClick }: Props) => {
  const borderStyle = isSelected ? "3px solid red" : "1px solid gray";
  return (
    <div
      style={{
        width: "30px",
        height: "30px",
        border: borderStyle,
        borderRadius: "50%",
        textAlign: "center",
        userSelect: "none",
        lineHeight: "30px",
      }}
      onClick={() => onClick(`${children}`)}
    >
      {children}
    </div>
  );
};

ボタンを押すと1万個のデータをフィルタリングするため、とても処理が多く、性能が悪いPCで試すと、画面に反映するまでにとても時間がかかってしまいます。
開発ツールで、CPUを落として試すと、画面表示までに2秒くらいかかってしまいます。
スクリーンショット 2022-11-05 22.39.23.png

トランジション機能を使ってみる(startTransition())

最初に、トランジション機能を使うために、startTransitionをインポートします。

import { startTransition } from "react";

トランジション機能を使う箇所で、startTransition()を書いて、中に関数を書きます。
中に書く関数は緊急性の低い関数を書きます。

startTransition(() => {});

下記は、トランジション機能を使ったコードになります。
選択されたものをすぐに画面に表示したいため、緊急性の低く、重い処理であるフィルタリング処理にトランジション機能を使いました。

import { useState, startTransition } from "react";
import { Avatar } from "./Avatar";

type Task = {
  id: number;
  title: string;
  assignee: string;
};

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

const generateDummyTasks = (): Task[] => {
  return Array(10000)
    .fill("")
    .map((_, index) => {
      return {
        id: index,
        title: `タスク${index}`,
        assignee:
          index % 3 === 0 ? member.a : index % 2 === 0 ? member.b : member.c,
      };
    });
};
const tasks = generateDummyTasks();

export const Transition = () => {
  const [selectedAssignee, setSelectedAssignee] = useState<string>("");
  const [taskList, setTaskList] = useState<Task[]>(tasks);

  const onClickAssignee = (assignee: string) => {
    // ユーザーに選択されたものをすぐに教えたいので、startTransition()の外に書く
    setSelectedAssignee(assignee);

    // 緊急性が低いものをtartTransition()の中に書く
    startTransition(() => {
      setTaskList(filteringAssignee(assignee));
    });
  };

  const filteringAssignee = (assignee: string) => {
    if (assignee === "") return tasks;
    return tasks.filter((task) => task.assignee === assignee);
  };

  return (
    <div>
      <p>Transition</p>
      <div style={{ display: "flex", justifyContent: "center", gap: "5px" }}>
        <Avatar
          isSelected={selectedAssignee === member.a}
          onClick={onClickAssignee}
        >
          {member.a}
        </Avatar>
        <Avatar
          isSelected={selectedAssignee === member.b}
          onClick={onClickAssignee}
        >
          {member.b}
        </Avatar>
        <Avatar
          isSelected={selectedAssignee === member.c}
          onClick={onClickAssignee}
        >
          {member.c}
        </Avatar>
      </div>
      <button onClick={() => onClickAssignee("")}>リセット</button>
      {taskList.map((task) => {
        return (
          <div
            key={task.id}
            style={{
              width: "300px",
              margin: "auto",
              backgroundColor: "skyblue",
            }}
          >
            <p>タイトル: {task.title}</p>
            <p>担当者: {task.assignee}</p>
          </div>
        );
      })}
    </div>
  );
};

画面で確認すると選択されたものはすぐに画面に反映され、重い処理であるフィルタリング処理はあとから表示されるようになりました。

FireShot Capture 115 - React App - localhost.png

useTransition()を使ってみる

トランジション機能を使う別の方法として、React18では、React HooksのuseTransition()が用意されています。
useTransition()を使うと、更新中か判断できるフラグを利用することができます。

useTransitionは、useTransitionをインポートし、useTransition()を使います。

import { useTransition } from "react";

// isPending: 更新中か判断できるフラグ
const [isPending, startTransition] = useTransition();

実際のコードはこちら。トランジション中は、色が薄くなるようにopacityにisPendingフラグをもたせました。

import { useState, useTransition } from "react"; //追加: useTransition

import { Avatar } from "./Avatar";

type Task = {
  id: number;
  title: string;
  assignee: string;
};

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

const generateDummyTasks = (): Task[] => {
  return Array(10000)
    .fill("")
    .map((_, index) => {
      return {
        id: index,
        title: `タスク${index}`,
        assignee:
          index % 3 === 0 ? member.a : index % 2 === 0 ? member.b : member.c,
      };
    });
};
const tasks = generateDummyTasks();

export const Transition = () => {
  const [isPending, startTransition] = useTransition(); //追加: useTransition
  const [selectedAssignee, setSelectedAssignee] = useState<string>("");
  const [taskList, setTaskList] = useState<Task[]>(tasks);

  const onClickAssignee = (assignee: string) => {
    // ユーザーに選択されたものをすぐに教えたいので、startTransition()の外に書く
    setSelectedAssignee(assignee);

    // 緊急性が低いものをtartTransition()の中に書く
    startTransition(() => {
      setTaskList(filteringAssignee(assignee));
    });
  };

  const filteringAssignee = (assignee: string) => {
    if (assignee === "") return tasks;
    return tasks.filter((task) => task.assignee === assignee);
  };

  return (
    <div>
      <p>Transition</p>
      <div style={{ display: "flex", justifyContent: "center", gap: "5px" }}>
        <Avatar
          isSelected={selectedAssignee === member.a}
          onClick={onClickAssignee}
        >
          {member.a}
        </Avatar>
        <Avatar
          isSelected={selectedAssignee === member.b}
          onClick={onClickAssignee}
        >
          {member.b}
        </Avatar>
        <Avatar
          isSelected={selectedAssignee === member.c}
          onClick={onClickAssignee}
        >
          {member.c}
        </Avatar>
      </div>
      <button onClick={() => onClickAssignee("")}>リセット</button>
      {taskList.map((task) => {
        return (
          <div
            key={task.id}
            style={{
              width: "300px",
              margin: "auto",
              backgroundColor: "skyblue",
              // 追加: 更新中は色を薄くする
              opacity: isPending ? 0.5 : 1,
            }}
          >
            <p>タイトル: {task.title}</p>
            <p>担当者: {task.assignee}</p>
          </div>
        );
      })}
    </div>
  );
};

useDeferredValue()を使ってみる

useDeferredValue()を使うことでも、トランジション機能を使うことができます。
useTransition()は、更新中のフラグがありましたが、useDeferredValue()は更新中のフラグありません。

import { useDeferredValue } from "react";

// state: トランジションさせたい緊急性の低いSET関数の結果が変わってくるステート
  const deferredvalue = useDeferredValue(state);

useDeferredValue()を使った実際のコードはこちら。

typescript src/components/TaskList.tsx
import { memo, useDeferredValue } from "react"; // 追加: useDeferredValue
import type { Task } from "./Transition";

type Props = {
  taskList: Task[];
};

export const TaskList = memo(({ taskList }: Props) => {
  // taskList: 緊急性の低いSET関数で結果が変わってくるステート
  const deferredTaskList = useDeferredValue(taskList); // 追加: useDeferredValue
  return (
    <>
      {deferredTaskList.map((task) => {
        return (
          <div
            key={task.id}
            style={{
              width: "300px",
              margin: "auto",
              backgroundColor: "skyblue",
              opacity: 1,
            }}
          >
            <p>タイトル: {task.title}</p>
            <p>担当者: {task.assignee}</p>
          </div>
        );
      })}
    </>
  );
});
src/components/Transition.tsx
import { useState } from "react"; //追加: useTransition
import { Avatar } from "./Avatar";
import { TaskList } from "./TaskList";

export type Task = {
  id: number;
  title: string;
  assignee: string;
};

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

const generateDummyTasks = (): Task[] => {
  return Array(10000)
    .fill("")
    .map((_, index) => {
      return {
        id: index,
        title: `タスク${index}`,
        assignee:
          index % 3 === 0 ? member.a : index % 2 === 0 ? member.b : member.c,
      };
    });
};
const tasks = generateDummyTasks();

export const Transition = () => {
  const [selectedAssignee, setSelectedAssignee] = useState<string>("");
  const [taskList, setTaskList] = useState<Task[]>(tasks);

  const onClickAssignee = (assignee: string) => {
    setSelectedAssignee(assignee);
    setTaskList(filteringAssignee(assignee));
  };

  const filteringAssignee = (assignee: string) => {
    if (assignee === "") return tasks;
    return tasks.filter((task) => task.assignee === assignee);
  };

  return (
    <div>
      <p>Transition</p>
      <div style={{ display: "flex", justifyContent: "center", gap: "5px" }}>
        <Avatar
          isSelected={selectedAssignee === member.a}
          onClick={onClickAssignee}
        >
          {member.a}
        </Avatar>
        <Avatar
          isSelected={selectedAssignee === member.b}
          onClick={onClickAssignee}
        >
          {member.b}
        </Avatar>
        <Avatar
          isSelected={selectedAssignee === member.c}
          onClick={onClickAssignee}
        >
          {member.c}
        </Avatar>
      </div>
      <button onClick={() => onClickAssignee("")}>リセット</button>
      <TaskList taskList={taskList} /> //緊急性の低いSET関数の値を渡している
    </div>
  );
};

画面で確認すると、トランジションされていることがわかりました。

スクリーンショット 2022-11-06 12.22.19.png

まとめ

  • React18で追加された自動バッチングとトランジション機能と変更点を手元で動かして確認しました。
  • トランジション機能を導入するとはパフォーマンス向上するので、プロジェクトに合わせて導入したいですね。

参考

10
6
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
10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?