React18
やりたいこと
以下の調査
- Suspense
- Lazy loading
- その他18の追加機能
- React 17との差分
上記によると、React18の具体的な新機能は以下
- レンダリングのバッチ化
- 複数の状態更新による再レンダリングをまとめて実行
- パフォーマンス向上
- トランジションの導入
- 再レンダリングを非同期で処理が可能になった
- ユーザーの操作を阻害せずに重いレンダリングを裏側で実行可能になった
- startTransition
- useTransitionでisPendingを取得可能
- isPendingを利用することで、ローディング表示を切り替えることができる
- <Suspense/>のサーバサイドレンダリング対応
import {startTransition} from 'react';
// 同期
setInputValue(input);
// 非同期
startTransition(() => {
setSearchQuery(input);
});
メモ
Suspense, lazyloadが使えていないだけでなく、useMemoを雰囲気で使ってしまっている為、
Reactのパフォーマンスについての解像度が低い。
React17とReact18の差分
React18へのバージョンアップについて
react-domのrender → react-dom/clientのcreateRoot()に変更
React 17
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);
React 18
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<App tab="home" />);
React周辺パッケージのアップグレード
framer-motion 4 → 6
react-dnd 15 → 16
@testing-library/react 12 → 13
型の変更
React.FC の props に暗黙的に children が含まれなくなったので、該当箇所で children を明示。
型の固定
@types/react、@types/react-dom のバージョンを固定する。
実際にReact18の環境を作って触ってみる
React.lazy?Lazy Import?lazy loading?
React.lazy、lazy import、lazy loadingなどのキーワードが出てくる
React.lazyとlazy importとlazy loadingがごっちゃになっていたけど、
React.lazyをつかったdynamic importをlazy importと呼んでいて、結果的にloadingがlazyになっているということで落とし込めた。
書き方の変更点
- rootの書き方
- react-dom/client
- ReactDOM.createRoot + render()
- strictモードで2回実行
- 18から全体のパフォーマンスが向上した為、アンマウント、再マウントが自動で行われる
- 純粋関数で実装されていれば、特に問題ないことを示すため
- トランジション
- 緊急度の高い更新、高くない更新を区別し、高い更新を優先的に行う機能
- useTransition(useStateのような使い心地)
- useDeferredValue
- 参考
StrictMode
Reactではレンダリングが2回実行
関数の冪等性を保証する実装が必要。
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// 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();
Auto Batching
stateの更新が複数回呼ばれた際に更新をグループ化し、単一のレンダーとして処理するもの。
React17以前ではイベントハンドラの中は、これが適用されていたが、Promiseが実行されたあとのステート更新では、ステートの数だけ再レンダリングが実行されていた(バッチ処理が実行されていなかった)。
React18ではそれが改善されている。
Transition
処理性能が低いマシンに対して有効な機能
優先度の高いステート更新を時間差で更新し、緊急性の高いステート更新を優先するためのもの
ユーザーの体験を劇的に変える
startTransitionをそのまま利用した場合
import { useState, startTransition } from 'react';
const onClickAssignee = (assignee: string) => {
setSelectedAssignee(assignee)
// 緊急性が高くないステート更新(トランジションさせたいもの)をいれる
startTransition(() => {
setTaskList(filteringAssignee(assignee))
})
}
useTransitionを利用した場合
import { useState, useTransition } from 'react';
export const Component = () => {
// 分割代入の第二引数がstartTransitionと全く同じ挙動
// 第一引数のisPendingにtransition中かどうかのフラグが入る
const [isPending, startTransition] = useTransition()
const onClickAssignee = (assignee: string) => {
setSelectedAssignee(assignee)
startTransition(() => {
setTaskList(filteringAssignee(assignee))
})
}
}
useDeferredValue
useTransitionの場合、set関数を囲う必要があったが、
子コンポーネント側でTransition処理を扱いたい場合、set関数を囲うといったことがやりにくい。
そういった場合に useDefferedValue を利用する。
使い方は遅延をかけたいステートを一旦、useDefferedValueの引数として渡し、戻り値を利用する。
import type { Task } from './Transition'
import { useDeferredValue } from 'react'
type Props = {
taskList: Task[];
}
export const TaskList = ({taskList}: Props) => {
const deferredTaskList = useDeferredValue(taskList)
return (
<>
{deferredTaskList.map((task) => (
<div key={task.id} style={{width: '300px', margin: 'auto', background: 'lavender'}}>
<p>タイトル:{task.title}</p>
<p>担当:{task.assignee}</p>
</div>
))}</>
)
}
Suspense
16.6で追加された機能だが、React.lazyと併用する必要があった。
React18からはReact.lazyの必要がない。
↓React.lazyと併用したこれまでの記述
import {lazy, Suspense} from 'react'
const OtherComponent = lazy(() => import('./OtherComponent'))
function MyComponent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
)
}
↓React18からのSuspense
React.lazyが不要になった。(ただし、SWR また TanstackQueryを用いる必要がある。)
import React, {Suspense} from "react";
import "./App.css";
import { ReactQuery } from "./components/ReactQuery";
function App() {
return (
<div className="App">
<Suspense fallback={<div>Loading...</div>}>
<ReactQuery />
</Suspense>
</div>
);
}
export default App;
↓Suspenseで囲われているコンポーネント
import {useQuery} from '@tanstack/react-query';
import axios from 'axios';
type Album = {
userId: number;
id: number;
title: string;
};
const fetchAlbums = async () => {
const result = await axios.get<Album[]>('https://jsonplaceholder.typicode.com/albums')
return result.data
}
export const ReactQuery = () => {
// suspenseを利用するフラグが必要
const { isLoading, error, data } = useQuery<Album[]>(['albums'], fetchAlbums, {suspense: true})
// if(error) return <div>Error</div>
// if(isLoading) return <div>Loading...</div>
return (
<div>
<p>React Query</p>
{data?.map((album) => (<p key={album.id}>{album.title}</p>))}
</div>
)
}