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?

More than 1 year has passed since last update.

React v18の機能について

Last updated at Posted at 2023-06-10

はじめに

こちらはReact V18から利用できる機能について解説した記事になります。
具体的な機能については以下が挙げられます。

  • Automatic Batching(自動バッチング)
  • Transitions(トランジション)
  • Suspense(サスペンス)

他にもストリーミングHTML選択的ハイドレーションといった機能がありますが、その詳細については素晴らしい記事がありましたので、以下の記事をご覧になってみてください。

また、以降の引用は公式ドキュメントから参照したものになります。

concurrent renderer(並行処理機能)とは

今回ピックアップした機能の解説の前に、concurrent renderer(並行処理機能)について説明します。

公式ドキュメントによると

React 18 の機能の多くが基盤としているのは新たに加わった並行レンダラ (concurrent renderer) であり、これが強力な新機能群を実現するために裏で働くようになっています。React の並行処理機能はオプトインであり、並行処理機能を使う場合にのみ有効になるものですが、これは皆さんのアプリ作成方法に大きな影響を与えるものであると思っています。

React v18以前においては、一度始まったレンダリングは必ず最後まで行われてから次のレンダリングに移行していました。一度始まったレンダリングを中断することはできず、またそのレンダリングが完了するまで別のレンダリングを始めることはできません。

React v18ではレンダリングの実行中に別のレンダリングを始めたり、レンダリングを途中で停止して破棄することができるようになりました。これがconcurrent renderer(並行処理機能)です。

そして今回解説する主要な機能は裏側でconcurrent rendererが動いていて、concurrent rendererによって実現されています。

では今回ピックアップした機能について解説していきます。

Automatic Batching

公式ドキュメントによると

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

AutoBatch.tsx
export const AutoBatchOther = () => {
  console.log('AutoBatchOther!!');

  const [todos, setTodos] = useState<Todo[] | null>(null);
  const [isFinishApi, setIsFinishApi] = useState<boolean>(false);

  // Promise内等イベントハンドラ以外の場所ではAitomatic Batchingされていなかった
  const onClickExecuteApi = () => {
    fetch('https://jsonplaceholder.typicode.com/todos')
      .then((res) => res.json())
      .then((data) => {
        setTodos(data);
        setIsFinishApi(true);
      });
  };

  // 省略
};

React v18以前は、イベントハンドラ以外で更新関数が複数行呼び出されると、その行数分コンポーネントが再レンダリングされていました。つまり上記のコードを例にするとonClickExecuteApiが呼び出されると、promiseチェーン内で更新関数が2行記述されているので、2回再レンダリングが発生していました。

しかしv18以降は、イベントハンドラ外に更新関数が複数行あってもバッチ処理として処理が行われるようになりました。したがってonClickExecuteApiが呼び出された場合更新関数がバッチ処理としてまとめられるので、再レンダリングは1回で済むようになりパフォーマンスが向上しています。

Transitions

公式ドキュメントによると

トランジション(transition; 段階的推移)とは React における新たな概念であり、緊急性の高い更新 (urgent update) と高くない更新 (non-urgent update) を区別するためのものです。

  • 緊急性の高い更新とはタイプ、クリック、プレスといったユーザ操作を直接反映するものです。
  • トランジションによる更新は UI をある画面から別の画面に段階的に遷移させるものです。

例を挙げると、ユーザーがフォームや検索画面の<input>タグに対してクリック、入力するときは即時反映が求められるので更新の優先度が高いですが、<input>タグに入力したキーワードをもとにしてAPIやDBから結果を取得し、ブラウザに表示されるような副作用による更新は優先度が低いと考えられます。

Reactから更新の優先度を区別する方法として、useTransitionuseDeferredValueの2つのHookが提供されています。

まずuseTransitionについて説明します。

1. useTransition

useTransition と startTransition により、一部の更新は緊急性が低いということをマークできるようになります。その他の更新はデフォルトで緊急性が高いものとして扱われます。React は緊急性の高い更新(例えばテキスト入力の更新)が、緊急性の低い更新(例えば検索結果のリストのレンダー)を中断できるようになります。

useTransitionは戻り値に、ローディング状態を監視するisPendingとラップされた更新関数の優先度を下げるstartTransitionが渡ってきます。

import { useTransition } from 'react';

function App() {
  const [isPending, startTransition] = useTransition();
  const [data, setData] = useState(null);

  const fetchData = () => {
    startTransition(() => {
      fetch('https://api.example.com/data')
        .then(response => response.json())
        .then(result => setData(result));
    });
  };

  return (
    <div>
      {isPending ? (
        <p>Loading...</p>
      ) : (
        <button onClick={fetchData}>Fetch Data</button>
      )}
      {data && <p>{data}</p>}
    </div>
  );
}

export default App;

上記の例では、fetchData関数がstartTransition内で呼ばれています。これにより、fetchリクエストが非同期に実行され、setDataによって状態が更新されます。isPendingがtrueの間、"Loading..."と表示され、データの取得が完了すると結果が表示されます。

startTransitionでラップされたfetchリクエストが返ってきて、promiseチェーン内の処理を行っている最中に、他のstartTransitionでラップされていない更新関数が呼ばれた場合では、promiseチェーン内の処理は更新の優先度が低いので後回しにされることになります。

2. useDeferredValue

useDeferredValue により、ツリー内の緊急性の低い更新の再レンダーを遅延させることができます。デバウンス (debounce) に似ていますが、それと比べていくつかの利点があります。遅延時間が固定でないため、最初のレンダーが画面に反映された時点ですぐに遅延されていた方のレンダーを始められるのです。また遅延されたレンダーは中断可能であり、ユーザインプットをブロックしません。こちらのドキュメントを参照。

useDeferredValuestartTransitionで、更新の優先度を下げたい更新関数をラップできない状況で使用するケースが考えられます。

使い方としてはuseDeferredValueの引数に、更新の優先度を下げたい状態を渡しその戻り値をJSX内で使用します。

import { useDeferredValue } from 'react';
import type { Task } from './task';

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

export const TaskList = ({ taskList }: Props) => {
  const deferredTaskList = useDeferredValue(taskList);

  return (
    <>
      {deferredTaskList.map((task) => (
        <div key={task.title}>
          <p>タイトル{task.title}</p>
          <p>担当{task.assignee}</p>
        </div>
      ))}
    </>
  )
};

更新の優先度が低いtasklistという配列のデータが、PropsでTaskListに渡ってきています。このtasklistをuseDeferredValueの引数に渡し、その戻り値をJSX内でMAP関数で展開しています。

このように記述することで、例えばmapで展開されている最中にユーザーがinputタグに値を入力した場合はJSX内の処理を中断し、ユーザー側の入力の値の反映を優先させます。

Suspense

サスペンスにより、コンポーネントツリーの一部がまだ表示できない場合に、ロード中という状態を宣言的に記述できるようになります:
サスペンスにより、「UI ロード中状態」というものが、React のプログラミングモデルで宣言的に記述可能な主要コンセプトに昇格します。その上により高レベルな機能を構築していけるようになります。

index.jsx
// ReactやAppなどのimportは省略
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
    },
  },
});

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);
ReactQuery.jsx
import { Suspense, useState, useTransition } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
// コンポーネントのインポートは省略

export const ReactQuery = () => {
  return (
    <div>
      <ErrorBoundary fallback={<h1>Error...</h1>}>
        <Suspense fallback={<p>Loading...</p>}>
          <TodoList />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
};
TodoList.jsx
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

const fetchTodos = async () => {
  const result = await axios.get('https://jsonplaceholder.typicode.com/todos');
  return result.data;
};

export const TodoList = () => {
  const { data } = useQuery(['todos'], fetchTodos);

  return (
    <div>
      {data?.map((todo) => (
        <p key={todo.id}>{todo.title}</p>
      ))}
    </div>
  );
};

コードの説明ですが、react-queryのライブラリを使用してデータを取得すると仮定して、まずルートコンポーネントのindex.jsxでreact-queryに対しオプションでsuspenceを使うことを明示します(QueryClient関数のデフォルトオプションに指定)

次にApp配下の、ReactQueryコンポーネントでReactからインポートしたSuspenceコンポーネントを使って、ローディング中に表示したいJSXをfallbackに記述し、react-queryを使ってデータを取得するコンポーネントTodoListをラップしています。

エラー処理については、react-error-boundaryというライブラリからErrorBoundaryというコンポーネントをインポートし、こちらもエラー発生時に表示したいJSXをfallbackに記述し、TodoListをラップしています。

TodoListコンポーネントは、react-queryを利用してサーバーからデータを取得しJSXにそのデータを反映させています。

まとめ

並行レンダーは React における新しいパワフルなツールであり、サスペンス、トランジション、ストリーミング付きサーバーレンダリングといった新たな機能のほとんどはこれを活用して構築されています。しかし React 18 はこの新しい基盤の上に我々が構築しようとしているものの始まりに過ぎません。

と公式ドキュメントでも言われており、更にconcurrent renderer(並行処理機能)をベースにした様々な機能が今後も追加されていきそうです。

おわりに

最後まで記事をご覧いただきありがとうございました。

間違いなどありましたらご指摘いただけると幸いです:bow:

参考

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?