はじめに
こちらは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、ネイティブのイベントハンドラやその他あらゆるイベント内で起きる更新はデフォルトではバッチ処理されていませんでした。自動バッチングにより、これらの更新も自動でバッチ処理されるようになります:
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から更新の優先度を区別する方法として、useTransition
とuseDeferredValue
の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) に似ていますが、それと比べていくつかの利点があります。遅延時間が固定でないため、最初のレンダーが画面に反映された時点ですぐに遅延されていた方のレンダーを始められるのです。また遅延されたレンダーは中断可能であり、ユーザインプットをブロックしません。こちらのドキュメントを参照。
useDeferredValue
はstartTransition
で、更新の優先度を下げたい更新関数をラップできない状況で使用するケースが考えられます。
使い方としては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 のプログラミングモデルで宣言的に記述可能な主要コンセプトに昇格します。その上により高レベルな機能を構築していけるようになります。
// 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>
);
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>
);
};
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(並行処理機能)をベースにした様々な機能が今後も追加されていきそうです。
おわりに
最後まで記事をご覧いただきありがとうございました。
間違いなどありましたらご指摘いただけると幸いです
参考