UGA Boxxx

つぶやきの延長のつもりで、知ったこと思ったこと書いてます

【React】ざっくりReact18の新機能

React18の新機能について知る機会があり、それを参考に自分でもまとめてみた

reactjs.org

新機能は以下

  1. Automatic Batching
  2. Transitions
  3. Suspense and SSR

Automatic Batching

要約

React18からはBatching対象が広がる

詳細

Batchingは複数のstateを更新したときにレンダリングを1回にまとめる仕組み

例えば、下のコードではsetCountとsetClickedの2回レンダリングされるのではなく、handleClickで1回のレンダリングにまとめられる

count [count, setCount] = useState(0);
count [clicked, setClicked] = useState(false);

function handleClick() {
  setCount(count + 1);
  setClicked(true);
}

しかし、React17まではpromiseやcallback、timeoutはBatching対象でないので2回レンダリングされていた

fetch(...).then(() => {
  setCount(count + 1);
  setClicked(true);
});

element.addEventListener('click', () => {
  setCount(count + 1);
  setClicked(true);
});

setTimeout(() => {
  setCount(count + 1);
  setClicked(true);
}, 1000);

これらがReact18からはBatching対象となる

これにより、これまで明示的にオプトインしていたReactDOM.unstable_batchedUpdates()は不要になり、逆に、オプトアウトしたい場合にReactDOM.flushSync()を使う

下の例では2回レンダリングが行われることになる

import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCount(count + 1); // 再レンダリング
  });

  // DOMの更新

  flushSync(() => {
    setClicked(true); // 再レンダリング
  });
}

Transitions

要約

緊急度の高い更新と低い更新を分離する

詳細

例えば、リストの絞り込み機能

下のようなリストと入力フォームがあったとして

[                 ]
- chocolate
- toast
- tomato

「to」と入力した場合、数秒後にchocolateが除外されても特に問題はない

[ to            ]
- chocolate
- toast
- tomato

...

[ to            ]
- toast
- tomato

このとき、入力フォームのタイプされた文字は緊急度の高い更新だが、リストの更新は緊急度が低いといえる

Transitionsは緊急度の高い更新を緊急度の低い更新が邪魔しないように、緊急度の低い更新をバックグラウンドで行い、緊急度の高い更新が終わったら反映するしくみ

import { startTransition} from 'react';

// 緊急度が高い: タイプされた文字を表示
setInputValue(input);

startTransition(() => {
  // 緊急度が低い: 結果を表示
  setSearchQuery(input);
});

緊急度が低い更新を開始するにはstartTransition()を使う

また、新しいTransitionが始まると以前のTransitionは破棄される

Suspense and SSR

要約

SSRの課題を解消するStreaming HTML と Selective Hydration の機能ができた

Streaming HTMLはサーバーが細かい単位でコンポーネントを送信することができる仕組み Selective Hydrationはクライアントサイドがハイドレーションを細分化する仕組み

これらによりSuspenseの外側と内側でレンダリングが分割された

詳細

これまでのSSRの次のようなウォーターフォールの課題を解消する

  • サーバーサイドのデータフェッチが終わらないとレンダリングが始まらない
  • サーバーサイドでレンダリング全体が終わらないとレスポンス送信できない
  • クライアントサイドでブラウザがHTML全体をレンダリングしないとハイドレーションが始まらない
  • コンポーネントツリー全体がハイドレーションされないとインタラクティブにならない、補助的なコンテンツのデータフェッチが始まらない

仕組みとしては<Suspense>を使う

まず、<Suspense>で囲まれたコンテンツのデータフェッチを待たずにサーバーサイドからHTML出力される

その間、<Suspense>内はローディング中になっているが、SSRはそれで終わりではなく

データフェッチが終わった時点で次のような HTML と JavaScript がサーバーから送られ、前に送られてきた HTMLを更新する(Streaming HTML)

<div hidden id="comments">
  <!-- Comments -->
  <p>First comment</p>
  <p>Second comment</p>
</div>
<script>
  // This implementation is slightly simplified
  document
    .getElementById("sections-spinner")
    .replaceChildren(document.getElementById("comments"));
</script>

クライアント側は<Suspense>以外の部分を先にハイドレーションし、まだ読み込みが済んでいない部分と分離することができるので、ページをレンダリングするために必要な JavaScript コードが全部読み込まれないとハイドレーションを行えないという問題が解消される(Selective Hydration)

他参考

https://www.youtube.com/watch?v=bpVRWrrfM1M

https://zenn.dev/uhyo/articles/react-18-alpha-essentials