React v18 概要
React v18 が npm に公開されたので、新機能を概要だけまとめる
- Concurrent Rendering
- Automatic Batching
- Suspense
- Client and Server Rendering APIs
- StrictMode
- v18 へのアップデートに関して
参考:
Concurrent Rendering
コンポーネント内で Promise を throw するという設計をサポートし、それを柔軟に行えるいくつかの公開 API を提供する
Promise を throw することによるレンダリングの中断と Promise を解決することによるレンダリングの再開が行える
Promise は解決されると DOM にマウントされる
Concurrent Rendering という名前の通り、コンポーネントのレンダリングが並行処理で実行される
複数のレンダリングプロセスが中断と再開を切り替えながら処理を進めるイメージ
JS はシングルスレッドであり、並列処理しているわけではないので全体の処理速度が向上する機能ではない
Concurrent 自体は機能ではなく、React 内部実装の話なので、開発者が実装の詳細を知る必要はない
ただし、Concurrent に関する以下の公開 API は把握しておく必要がある
- Suspense
- v16 から使える機能で、React.lazy と組み合わせて使用する必要があった
- v18 からは機能拡張される(後述)
- useTransition(startTransition)
- 参考:https://qiita.com/uhyo/items/ba49b25f0a206e933e4d
-
[startTransition, isPending] = useTransition({ timeoutMs: 10000 })
- startTransition 関数で Promise を返す
- 変更前の State でレンダリングされる、isPending が true になる
- timeoutMs の時間(isPending が true の時間)を経過した場合は、変更後の State でレンダリングされる
- この時 Promise が解決されていなければ、Suspense の fallback などで対応する
- startTransition 関数の Promise が解決
- 変更後の State でレンダリングされる
- startTransition 関数を使っていない
- Suspense の fallback などで対応する
- startTransition 関数で Promise を返す
- useDeferredValue
- 優先度が低いレンダリングを遅延させられる
const [text, setText] = useState("hello"); const deferredText = useDeferredValue(text, { timeoutMs: 2000 });
- ユーザーのテキスト入力という優先度が高い処理は setText を使い即座に State 変更する
- 入力結果を画面表示させるという優先度が低い処理は、useDeferredValue フックでラップした deferredText を使い、React のスケジューラーに更新を委ねる(遅延更新)
- レンダリングされるまで変更前の State が使われる
- 優先度が低いレンダリングを遅延させられる
上記の API はいずれもレンダリング優先度の低い State 更新をラップし、React に遅延更新を通知するものだが、用途が異なるので使い分ける必要がある
たとえば、startTransition は State 更新の関数そのものをラップするが、useDeferredValue は State 更新の影響を受ける値をラップしている
Concurrent の実装に関して
参考:https://qiita.com/uhyo/items/4a6315bfccf387407631
- コンポーネントが Promise を保持する必要がある
- useState などで
- 実装レベルだと Promise では機能不足なので、ラップして SWR みたいな機能として提供する必要がある
- isLoading のような非同期処理の途中という State は不要になる
- Suspense で表現できる
- Promise を持つコンポーネントから返されるのは
T
(解決済)かPromise.throw
(未解決)なので、呼び出し元は戻り値T
だけ意識して実装できる-
Promise.throw
は Suspense で fallback を実装しておけば良い
-
- throw した Promise が reject した場合は、Error Boundary でキャッチされる
Automatic Batching
通常、1 つの State 更新が行われると再レンダリングが実行されるが、Batching は複数の State 更新を 1 回の再レンダリングで済むようにグループ化して、パフォーマンスを向上させる機能
また、片方の State だけ更新されて再レンダリングされてしまうみたいな不都合も防ぐことができる
React v17 以前はイベントハンドラー内でのみ Batching が機能していたが、v18 以降は setTimeout や Promise などのコールバック関数内でも自動で Batching 処理してくれるようになる
// 複数のState更新があっても1回の再レンダリングで済む
setTimeout(() => {
setCount((c) => c + 1);
setFlag((f) => !f);
}, 1000);
こちらの機能は v18 に移行後デフォルトで ON になるので、もしイベントハンドラー以外で複数 State を更新している箇所があれば一応確認したほうがいいのかも
(react-dom の flushSync を使って Batching をオプトアウトすることは可能)
Suspense
v17 以前の Suspense はReact.lazy
を使用したコード分割のみをサポートしていたが、v18 からは大幅に機能拡張されている
Client and Server Rendering APIs
export 先が変わっている API がある
- react-dom/client
- createRoot
- react-dom の render をこれに変更する必要あり
- hydrateRoot
- react-dom の hydrate を使っていたら、これに変更する
- createRoot
- react-dom/server
- ストリーム処理用の API が追加
- renderToPipeableStream
- renderToReadableStream
- renderToString は引き続き使えるが、オススメしないとのこと
- ストリーム処理用の API が追加
StrictMode
React の StrictMode は v17 以前からある機能で非推奨・レガシーな実装や副作用のある実装などを検出してくれる開発用のみで動くモードだが、既存動作に加えて以下の挙動が加わる
「コンポーネントの初回マウント時に、コンポーネントを自動的にアンマウント、および再マウントする」
よって、useEffect を使っていれば、2 回呼ばれるようになる
この挙動で意図する所は、useEffect 内の処理は冪等にすべきと解釈できる
たとえば、useEffect の第 2 引数の依存配列を空配列にしており、1 回のみ実行されることを想定して実装している場合、複数回の useEffect 実行で意図しない結果になる可能性がある
これは Offscreen という API が将来追加予定となっており、レンダリングされた DOM の状態を保持したまま画面非表示にすることが可能になるため、この仕様に耐えうる実装であるかをシミュレートできる
Offscreen を使うユースケースとしては、ブラウザのタブ切り替えなどがある
v18 へのアップデートに関して
v17 と比べ、v18 は新機能や新 hook が多く追加されているので、また改めて検証しないとなという感じ
また、Suspense や StrictMode などの既存 API も大きな変更が行われているが、これらはほぼ必要であればその機能を export して使うというオプトインが可能なので、(使わなければ)移行のコストは低いと思う
v18 移行で対応が必要な箇所は以下にまとまっている
v18 は IE をサポートしない(もし、IE をサポートするなら v17 以下を推奨)なので、このタイミングで IE を切るビジネス判断を行いたい