Closed7

React v18 概要

kurosamekurosame

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 などで対応する
  • 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 でキャッチされる
kurosamekurosame

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 をオプトアウトすることは可能)

kurosamekurosame

Client and Server Rendering APIs

export 先が変わっている API がある

  • react-dom/client
    • createRoot
      • react-dom の render をこれに変更する必要あり
    • hydrateRoot
      • react-dom の hydrate を使っていたら、これに変更する
  • react-dom/server
    • ストリーム処理用の API が追加
      • renderToPipeableStream
      • renderToReadableStream
    • renderToString は引き続き使えるが、オススメしないとのこと
kurosamekurosame

StrictMode

React の StrictMode は v17 以前からある機能で非推奨・レガシーな実装や副作用のある実装などを検出してくれる開発用のみで動くモードだが、既存動作に加えて以下の挙動が加わる

「コンポーネントの初回マウント時に、コンポーネントを自動的にアンマウント、および再マウントする」

よって、useEffect を使っていれば、2 回呼ばれるようになる
この挙動で意図する所は、useEffect 内の処理は冪等にすべきと解釈できる
たとえば、useEffect の第 2 引数の依存配列を空配列にしており、1 回のみ実行されることを想定して実装している場合、複数回の useEffect 実行で意図しない結果になる可能性がある

これは Offscreen という API が将来追加予定となっており、レンダリングされた DOM の状態を保持したまま画面非表示にすることが可能になるため、この仕様に耐えうる実装であるかをシミュレートできる
Offscreen を使うユースケースとしては、ブラウザのタブ切り替えなどがある

kurosamekurosame

v18 へのアップデートに関して

v17 と比べ、v18 は新機能や新 hook が多く追加されているので、また改めて検証しないとなという感じ
また、Suspense や StrictMode などの既存 API も大きな変更が行われているが、これらはほぼ必要であればその機能を export して使うというオプトインが可能なので、(使わなければ)移行のコストは低いと思う

v18 移行で対応が必要な箇所は以下にまとまっている
https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html

v18 は IE をサポートしない(もし、IE をサポートするなら v17 以下を推奨)なので、このタイミングで IE を切るビジネス判断を行いたい

このスクラップは2022/04/01にクローズされました