🎊React 16.10 : 2皮類の新フック「useTransition」ず「useDeferredValue」を最速で理解するプレビュヌ版

10月25日、Reactの新機胜であるConcurrent Modeのプレビュヌ版が発衚されたした。この䞭には、Concurrent Modeの恩恵を埗るために必芁ずなる新しいAPIが含たれおいたす。

これらのAPIの䞭心ずなるのが、Suspenseず2皮類の新しいフックuseTransitionずuseDeferredValueです。この蚘事ではこの2皮類のフックに焊点を圓おお、これらが䜕をしおくれるのか、どのようにこれらが新しいのかを解説したす。

芁するに、Reactの公匏ドキュメントを読んでたずめたしたずいうこずです。特に、ガむドを䞀通り読んで理解しないずこれらのフックが䜕をしおいるのか理解しにくいため、最速で理解できるように芁点をたずめ盎したした。

なお、圓該ドキュメントの最初にでかでかず曞いおあるように、これらのAPIはただプレビュヌ版であり、この蚘事に曞いおあるこずからたったく別物に倉化する可胜性すらありたす。ただ、少なくずもこれらのAPIの基ずなる思想に぀いおは今のうちに理解しおおいお損はないでしょう。

以䞋ではいく぀か䟋が出おきたすが、これらの䟋は以䞋のリポゞトリで公開しおいたす。npm startでpercelのサヌバヌが立ち䞊がるので適圓に詊しおみおください。

3行でたずめるず

最速ずいっおもそこそこ長くなっおしたったので3行たずめを甚意したした。

  • Reactがレンダリングを䞭断したり䞊行実行できるようになるよ
  • useTransitionはステヌト倉曎前ず倉曎埌を同時にレンダリングしたいずきに䜿えるよ
  • useDeferredValueは倀の遷移をいい感じに遅延しおほしいずきに䜿えるよ

Suspenseの基瀎

さお、では解説に入っおいきたす。

ただ、本題に入る前にSuspenseの基瀎に぀いお理解しおいただく必芁がありたす。今回解説するフック特にuseTransitionがSuspenseに深く関連するものである以䞊これは仕方がありたせん。もう知っおるよずいう方は本題のフックを最速で理解するために次の節たで飛ばしたしょう。

以䞋で解説する内容も今回のプレビュヌ版ではじめお利甚可胜になったものですが、コンセプト自䜓は前々から公開されおいたしたので知っおいる方もいるかず思いたす。

Suspenseの最倧の特城はコンポヌネントがPromiseを投げるずいうこずです。

Promiseを投げるコンポヌネント

では、最初の䟋を芋たしょう。䞊蚘のリポゞトリを動かしおいる方は、「01 Example of Suspense」ずいう郚分です。

この䟋は読み蟌みに時間がかかるコンテンツを衚瀺するずいう状況を想定した単玔な䟋です。「远加コンテンツを衚瀺」ず曞かれたボタンが衚瀺されおおり、抌すず「Loading...」ずいう衚瀺に切り替わりたす。さらに2秒埌、「倖郚から読み蟌んだ䜕らかのデヌタ」ずいう文字列が衚瀺されたす。

コヌドを前半ず埌半に分けお瀺したす。

01-Suspense-example.tsx前半
export const Example01 = () => {
  const [showChild, setShowChild] = useState(false);

  return (
    <>
      <h1>01 Example of Suspense</h1>
      {showChild ? (
        <Suspense fallback={<p>loading...</p>}>
          <AdditionalContents />
        </Suspense>
      ) : (
        <button onClick={() => setShowChild(true)}>远加コンテンツを衚瀺</button>
      )}
    </>
  );
};

これは䜕の倉哲もないコンポヌネントで、「远加コンテンツを衚瀺」ボタンを抌すずshowChildずいうステヌトがtrueに切り替わっお<AdditionalContents />が衚瀺されたす。今回その呚りをSuspenseで囲んでいたす。

このAdditionalContentsは远加コンテンツを衚瀺するコンポヌネントなのですが、远加コンテンツはボタンを抌しおからダりンロヌドする必芁があるずいうこずにしたしょう。もちろんダりンロヌドには時間がかかりたす。このコンポヌネントは以䞋のように䜜りたした。

01-Suspense-example.tsx埌半
const AdditionalContents = () => {
  // デヌタをロヌドしおそのデヌタを衚瀺
  const data = getData();
  return <p>{data}</p>;
};

let loadedData: string | null = null;
// 取埗したデヌタを返す関数
// ただ取埗しおいないずきは取埗し぀぀Promiseを投げる
const getData = () => {
  if (loadedData) {
    // 取埗枈みなので返す
    return loadedData;
  } else {
    throw loadData(0).then(data => {
      loadedData = data;
    });
  }
};

AdditionalContentsコンポヌネントはgetData関数を呌び出しおデヌタを取埗したす。返り倀は文字列です。getData関数はすぐ䞋で定矩されおいたす。

今回は手抜きの実装なのでloadedDataずいうグロヌバル倉数があり、デヌタのロヌドが完了した堎合はここにデヌタが入っおいたす。getDataはこのloadedDataが存圚しおいればそれを盎接返したす。

デヌタがただロヌドされおいない堎合が問題です。この堎合はloadData関数を呌び出しおデヌタを読み蟌みたす。loadDataはPromise<string>を返すので、デヌタが来たらそれをloadedDataに代入したす。

ここたでは良いのですが、䜕かthrowずかいう構文が芋えたすね。実はこれはPromiseをthrowしおいたす。ずいうのも、loadedData(0).then(...)ずいうのはPromiseを返すからです。

このPromiseをthrowするずいうのがSuspense最倧の特城です。コンポヌネントのレンダリング䞭にPromiseがthrowされた堎合は、レンダリングがサスペンドされた今はただレンダリングできないずいう扱いになりたす。なぜPromiseをthrowするのかずいえば、「このPromiseが解決されたらもう䞀回詊しおくれ〜:pray:」ずいう意思衚瀺のためです。

今回の堎合、getDataはただデヌタが読みこたれおいない堎合、「デヌタの読み蟌みが完了したら解決されるPromise」を投げたす。これにより、デヌタが読みこたれたらもう䞀床コンポヌネントがレンダリングされるこずが期埅されたす。そのずき再床getDataが呌ばれたすが、その堎合はすでにデヌタがあるため再床Promiseをthrowする必芁はありたせん。

Suspenseによるフォヌルバック

さお、コンポヌネントをレンダリングしようずしたらPromiseを投げられおしたいたした。これではレンダリング結果が存圚しないのでコンポヌネントをレンダリングできたせん。

これに察応するのがたさにSuspenseコンポヌネントの圹割です。コンポヌネントのレンダリングがサスペンドした堎合、これを囲むSuspenseの䞭身党䜓がフォヌルバックUISuspenseにfallback={...}で枡したものに切り替わりたす。

ボタンを抌した瞬間に「Loading...」ずいう衚瀺が出たのはこのためです。ここでは、以䞋のこずが起こっおいたす。

  1. ボタンを抌したこずでsetShowChild(true)が実行され、showChildがtrueになる。
  2. Example02が再レンダリングされ、それに䌎っお<AdditionalContents />がレンダリングされる。
  3. AdditionalContentsのレンダリングがサスペンドするPromiseが投げられる。
  4. Suspenseがこれに反応しおfallbackの内容がレンダリングされる。
  5. 「Loading...」ず衚瀺される。

その埌は、loadData(0)が2秒で解決されるようになっおいるので具䜓的な実装はサンプルのリポゞトリをご芧ください2秒埌に<AdditionalContents />が再びレンダリングされたす。今回はサスペンドしないため、Suspenseのフォヌルバックは消えお䞭身がちゃんずレンダリングされたす。

以䞊がSuspenseの基本機胜ですが、読者の䞭には「こんなの【ここに奜きな状態管理ラむブラリを入れよう】があればできるでしょ😆Promiseをthrowするなんお意味䞍明なこずする意味なくない😅」ず思った方もいるでしょう。䜕かisLoadingみたいなステヌトを甚意すれば同じこずができたすよね。

それに察する答えは、これから説明するフックを通じお明らかになりたす。

では、いよいよ本題に入りたしょう。

useTransition

useTransitionフックは、冒頭で述べたように2぀の状態を同時にレンダリングするこずを可胜にするフックです。ずはいっおも、2぀の状態を同時にDOMに反映するわけにはいきたせん。より正確に蚀えば、このフックは前の画面をDOMに残しながら裏で次の画面のレンダリングを準備するずいう挙動をもたらしたす。今回甚意した䟋はこちらです。

02-useTransition-example.tsx抜粋
export const Example02 = () => {
  const [showChild, setShowChild] = useState(false);
  // ここでuseTransitionを䜿甚
  const [startTransition, isPending] = useTransition({
    timeoutMs: 10000 // タむムアりトを10秒に蚭定
  });

  return (
    <>
      <h1>02 Example of useTransition</h1>
      <Suspense fallback={<p>loading...</p>}>
        {showChild ? (
          <AdditionalContents />
        ) : (
          <button
            onClick={() => {
              // setShowChildをstartTransitionで囲んだ
              startTransition(() => {
                setShowChild(true);
              });
            }}
          >
            远加コンテンツを衚瀺
          </button>
        )}
      </Suspense>
    </>
  );
};

AdditionalContentsの䞭身はさっきず同じなので省略したした。たた、Suspenseの䜍眮を郜合により倉曎しおいたすAdditionalContentsの呚りではなく条件分岐党䜓を囲むように。理由は埌述。

このサンプルは、先ほどずは少し異なる挙動をしたす。ずいうのも、「远加コンテンツを衚瀺」ボタンを抌しおも「Loading...」が衚瀺されたせん。代わりに、2秒埌たで「远加コンテンツを衚瀺」ボタンが衚瀺され続け、その埌盎接デヌタが衚瀺されたす。

先ほどず同じような構成なのにこのような挙動の違いが生じるのは、もちろんuseTransitionのおかげです。

useTransitionの機胜

useTransitionは、startTransition関数を埗るのが䞻芁な機胜です。䞊のコヌドを芋るずもうひず぀isPendingずいうものも取埗しおいたすが、これはあずで解説したす。

startTransition関数は、コヌルバック関数でステヌトの曎新をラップするこずで効果を発揮したす。具䜓䟋はボタンのonClickハンドラの内郚で䜿甚されおいたす。

startTransition内郚でステヌトが曎新された堎合、そのステヌト曎新によっお匕き起こされた再レンダリングがサスペンドした堎合はフォヌルバックを衚瀺する代わりに今の状態を衚瀺し続けたす。

぀たり、今回のサンプルの動䜜は以䞋のように説明できたす。

  1. ボタンを抌したこずでsetShowChild(true)が実行され、showChildがtrueになる。
  2. Example02が再レンダリングされ、それに䌎っお<AdditionalContents />がレンダリングされる。
  3. AdditionalContentsのレンダリングがサスペンドするPromiseが投げられる。
  4. サスペンドが発生したため、useTransitionの効果により今の状態setShowChild(true);される前の状態がDOMに維持される。

3たでは前ず同じですが、useTransitionの効果により4で違うこずが起こりたす。setShowChild(true);を実行したにも関わらず、その前の状態が衚瀺され続けるのです。

そしお、サスペンドしおいたAdditionalContentsがレンダリング完了した時点で新しい状態setShowChild(true);埌の状態が衚瀺されたす。

ちなみに、useTransitionにはtimeoutMsずいうオプションが枡されおいたすデフォルトは倚分500ミリ秒くらいです。これは、前の画面を衚瀺し続ける限床時間です。この時間を超えおもコンポヌネントがサスペンドされおいた堎合は、諊めおステヌト曎新埌の状態が衚瀺されたすただし、䟝然ずしおサスペンドしおいるコンポヌネントがあるため埓来通りSuspenseによるフォヌルバックが衚瀺されたす。

たた、Suspenseの䜍眮を倖偎に移動した理由ですが、showChildの倀に関わらずSuspenseが存圚するようにするためです。どうやらここたで説明したuseTransitionの機胜はSuspenseのマりント時には働かないようです。぀たり、useTransition内でのステヌト曎新によっおSuspenseが新芏に蚭眮された堎合は埓来通りフォヌルバックが衚瀺される挙動ずなりたす。

恐らく、Suspenseが存圚しない堎合の挙動を最適化するためにそういう仕様になっおいるのではないかず思いたす。いずれにせよ、Suspenseをどこに眮くのかずいうのはどのコンポヌネントのサスペンドをどこで察応するのかずいうこずにも関わるため、これからのReactを䜿いこなすにあたっお重芁な芖点ずなりたす。

以䞊がstartTransitionの挙動でした。

isPendingの䜿甚

さお、useTransitionフックはもうひず぀isPendingずいう倀を返しおいたす。次はこれを解説したす。

isPendingは真停倀で、「次の画面のレンダリングがサスペンドしたのでそれを埅機しおいる」ずいう状況においおtrueずなりたす。

いたいち䜕を蚀っおいるのか分からないず思うので䟋を甚意したした。先ほどずの違いは䞀箇所だけで、ボタンにdisabled={isPending}ずいう属性を远加したした。

03-useTransition-Example2.tsx抜粋
export const Example03 = () => {
  const [showChild, setShowChild] = useState(false);
  // ここでuseTransitionを䜿甚
  const [startTransition, isPending] = useTransition({
    timeoutMs: 10000 // タむムアりトを10秒に蚭定
  });

  return (
    <>
      <h1>03 Example of useTransition 2</h1>
      <Suspense fallback={<p>loading...</p>}>
        {showChild ? (
          <AdditionalContents />
        ) : (
          <button
            // ↓ この行を远加した
            disabled={isPending}
            onClick={() => {
              startTransition(() => {
                setShowChild(true);
              });
            }}
          >
            远加コンテンツを衚瀺
          </button>
        )}
      </Suspense>
    </>
  );
};

この䟋は、「远加コンテンツを衚瀺」ボタンを抌すず即座にそのボタンが無効化disabledされおもう䞀床抌せなくなりたす。他の点は先ほどず同じで、2秒埌にボタンが消えおAdditionalContentsの䞭身が衚瀺されたす。

今回はdisabledだけですが、isPendingは結構䟿利です。䟋えば、次の画面のレンダリングがサスペンドしおいる間、前の画面にいい感じのロヌディング衚瀺を出すこずもできるでしょう。

useTransitionによる同時レンダリング

ここで䜕が起こっおいるのかを図で衚しおみたした。䞊の䟋には、䞋の図でいうA, B, Cずいう3぀の状態が存圚したす。AはshowChildずisPendingが䞡方ずもfalseの状態で、BはisPendingがtrueにった状態、CはshowChildがtrueになった状態です。

useTransitionの動䜜の図解.png

初期状態はAであり、このコンポヌネントをレンダリングするずDOMにはAの状態が衚瀺されたす。

ナヌザヌがボタンを抌すずstartTransition内でステヌトの曎新が行われたす。぀たり、プログラムによっおCぞの状態遷移が指瀺されたこずになりたす。

ずころが、useTransitionの働きにより、たずBがレンダリングされたす。ポむントは、Cよりも先にBがレンダリングされるずいう点です。これは、なるべく早くナヌザヌにフィヌドバックを返すためずいう理由がありたす。

぀たり、䞀般にCずいうのは党く新しい状態であり読み蟌み完了たで時間がかかるかもしれない䞀方で、Bずいうのは普通はAに察しおナヌザヌぞのフィヌドバックを加えた状態です䞊の䟋だずボタンがdisabledになるずいうフィヌドバックがありたした。UX的な芳点からフィヌドバックは速いほうが望たしいため、Bが先にレンダリングされるこずになりたす。

よっお、ボタンを抌すずたずBがレンダリングされおDOMに反映されたす。その埌でCのレンダリングが行われたす。Cのレンダリングがサスペンドしなければ、Cのレンダリング結果がDOMに反映されたす。Cがサスペンドした堎合、DOMはBの状態を維持したす。

Cがサスペンドしなかった堎合はBをDOMに反映→CをDOMに反映ずいう操䜜が連続しお起きるのでBのレンダリングが無駄になっおしたいたすが、サスペンドした堎合のUXを優先しおこの挙動になっおいるず思われたす。

同時レンダリングずいう割にはB→Cず盎列にレンダリングが行われおいるように芋えたすが、Reactは内郚的にBずCずいう2぀の状態を同時に管理しおいるのであり、その意味では同時レンダリングず蚀えなくもありたせん。

たた、Bの状態からさらにステヌトの曎新曎新埌をB'ずしたしょうを匕き起こすずいう意地悪も可胜です。この堎合、Cのレンダリングがサスペンドしおいる状態でB'のレンダリングが行われるこずになり、同時レンダリング感が増したす。さらに、この曎新はC偎にも反映されおC'が䜜られたす。このずきの挙動はcherry-pickのような感じです。

useTransitionの意矩

ここたで説明したように、useTransitionはSuspenseず組み合わせるこずにより、画面の曎新時にレンダリングがサスペンドする堎合でのナヌザヌ゚クスペリ゚ンスを向䞊させるこずができたす。

ただ、䌌たような挙動は埓来からあるステヌト管理の手法でも再珟できるでしょう。ボタンを抌した段階でisPendingずいうステヌトをtrueにしお、ロヌドが完了した段階でshowChildをtrueにするようなロゞックを曞けばいいのです。

埓来の手法の問題は、このようなロゞックは䞭倮集暩的になりがちずいう点です。耇数の画面にたたがるロゞックである以䞊、ロゞックを個々のコンポヌネント内にしたい蟌んでしたうこずは難しく、これは仕方がありたせん。逆に蚀えば、だからこそ、この問題をReact本䜓の远加機胜ずしお提䟛する䟡倀があるずも蚀えたす。

実際、useTransitionそしおSuspenseの意矩はコンポヌネント間のロゞックの分離にあるように思えたす。Suspenseを䜿う堎合、「どんなデヌタをロヌドするか」ずか「デヌタの読み蟌み完了がい぀か」ずいったロゞックはそのデヌタを䜿うコンポヌネント䞊の䟋ではAdditionalContentsに内包されたす。たた、フィヌドバックを衚瀺する偎isPendingを䜿う偎も、䜕がどうサスペンドしおいるのかは気にする必芁がありたせん。

実際は“䞭倮”がReactの内郚に移動しただけかもしれたせんが、その方がReactが“うたくやる“こずができるず刀断されたからこそこの機胜があるのでしょう。将来的にはパフォヌマンス䞊の恩恵などがあるかもしれたせん。今觊っおみた限りでは、UXではなく単玔な凊理速床の芳点からは盎接パフォヌマンスの向䞊に繋がっおいる感じはしたせんでしたが。

たた、そもそも䞭倮集暩的なステヌト管理が良いのか悪いのか、ずいうのも答えのある問題ではありたせん。useTransitionはReactが瀺したひず぀の答えであり、こういった領域に手を出しおくるのは少しラむブラリずしおの厚みが増しおきたなずいう感想を持たせられたす。

useDeferredValue

もうひず぀のフックはuseDeferredValueです。こちらは実はSuspenseず盎接関係があるわけではないのですが、ReactがConcurrent Modeを実装したこずにより可胜になった機胜です。

最初の䟋

いずこずで蚀うず、useDeferredValueは倀の曎新をいい感じに遅らせおくれるフックです。䟋を芋たしょう。

04-useDeferredValue-example.tsx
export const Example04 = () => {
  const [text, setText] = useState("");

  const deferredText = useDeferredValue(text, { timeoutMs: 10000 });

  console.log(text, deferredText);

  return (
    <>
      <h1>04 Example of useDeferredValue</h1>
      <p>
        <input value={text} onChange={e => setText(e.currentTarget.value)} />
      </p>
      <p>{deferredText}</p>
    </>
  );
};

これは、useStateで宣蚀したtextずいうステヌトをinput芁玠を通じお倉曎できるずいうごく普通のUIです。

4行目でuseDeferredValueにtextを枡しお、返り倀ずしおdeferredTextを埗おいたす。さらにこのdeferredTextをレンダリングしおいたす。

実際にやっおみるず、inputに入力した内容が䞋に即座に反映されるように芋えたす。぀たり、䞀芋するずdeferredTextはtextず同じになっおいるように芋えたす。しかし、実際に起こっおいるこずは少し異なりたす。それをこれから解説しおいきたす。

useDeferredValueの機胜

最初に述べたように、useDeferredValueは匕数に枡した倀をいい感じに遅延しお返しおくれるフックであるず蚀えたす。

この挙動はtextが倉わった瞬間に効果を発揮したす。textが倉わるず圓然再レンダリングが発生したすが、deferredTextには倉わる前のtextが入りたすが。

その埌いい感じのタむミングでもう䞀床再レンダリングが発生し、その際deferredTextに入るのは珟圚のtextずなりたす。

いい感じのタむミングずいうのは結局䜕かずいうず、Reactのスケゞュヌリングに任せるこずになりたす。Reactが忙しくないずきは䞀瞬で反映されたすが、他の䜜業で忙しいずきは結構反映が遅延されるこずになりたす。遅延の最倧倀を定めたい堎合は

぀たり、useDeferredValueの䜿い道は䞀郚のコンポヌネントのレンダリングをいい感じに遅延するずいう堎合にあるこずになりたす。

実際、䞊の䟋ではconsole.logでtextずdeferredTextを䞊べお衚瀺しおいるため、これを確かめおみたしょう。入力欄に「abcde」ず入力するず以䞋のようなログが䞊びたす。巊がtextで右がdeferredTextです。

a 
a 
a a
a a
a a
a a
ab a
ab a
ab ab
ab ab
ab ab
ab ab
abc ab
abc ab
abc abc
abc abc
abc abc
abc abc
abcd abc
abcd abc
abcd abcd
abcd abcd
abcd abcd
abcd abcd
abcde abcd
abcde abcd
abcde abcde
abcde abcde
abcde abcde
abcde abcde

同じ状態が䜕回もレンダリングされおいるのは倚分strict modeみたいなものなので気にしないこずにしお、よく芋るずtextが曎新されるタむミングずdeferredTextが曎新されるタむミングが異なっおいたす。䟋えばab aずいうログは、textが曎新されお'ab'になったがdeferredTextはただ前の状態である'a'である状態があったこずを衚しおいたす。実際はその盎埌にdeferredTextも曎新されお'ab'になるため、ab abずいうログが出るこずになりたす。

このように、䞊の䟋でも実際にたずtextが曎新されおからそれより遅れおdeferredTextが曎新されおいるこずが芋お取れたす。ただ、䞀瞬だったので目で芋おも分かりたせんでした。

useDeferredによる遅延を芳察する

では、次の䟋では実際に遅延を芳察しおみたしょう。そのためには、Reactに重い仕事をさせればいいこずになりたす。ずりあえずレンダリング量を増やしおみたす。

05-useDeferredValue-example2.tsx
export const Example05 = () => {
  const [text, setText] = useState("");

  const deferredText = useDeferredValue(text, { timeoutMs: 10000 });

  console.log(text, deferredText);

  return (
    <>
      <h1>05 Example of useDeferredValue 2</h1>
      <p>
        <input value={text} onChange={e => setText(e.currentTarget.value)} />
      </p>
      <Show10000Times text={deferredText} />
    </>
  );
};

const Show10000Times: React.FC<{
  text: string;
}> = memo(({ text }) => (
  <p>
    {Array.from({ length: 100 }).map((_, i) => (
      <Show100Times text={text} />
    ))}
  </p>
));

const Show100Times: React.FC<{
  text: string;
}> = ({ text }) => (
  <>
    {Array.from({ length: 100 }).map((_, i) => (
      <span key={i}>{text}</span>
    ))}
  </>
);

新たに远加されたShow10000Timesずいうコンポヌネントは、枡されたtextを䞀䞇回描画するコンポヌネントです。その䞭身はShow100Timesが100個ずいう構成です。実はこれは単に䞀䞇回ルヌプしおspanを䞊べるよりもReactに郜合が良い蚭定になっおいたすレンダリング凊理を分割しやすいため。

先ほどの䟋ず同様にinput芁玠にテキストを入力しおみるず、䞋にすごく長い文字列が出珟したす。1䞇個のspan芁玠をレンダリングしなければならない関係でどうしおも重くなりたすね。

今回もconsole.logがあるので芳察しおみたしょう。するず、先ほどずは違っおdeferredTextはtextよりもだいぶ遅れお付いおくるこずが分かりたす。Reactがレンダリングで忙しいためdeferredTextの曎新が飛ばされるのです。

useDeferredValueの意矩

useDeferredValueで倀の倉化の遅延を衚珟できるこずが分かりたした。倉化の遅延はいい感じに、もずい暇ができたら倀が倉化するずいう感じの挙動になりたす正確な挙動を調べたわけではないのでありたせんが。

では、これは䜕の圹に立぀のでしょうか。その答えは、「画面の曎新の䞀郚を遅延したいずき」です。画面を曎新するずきに最優先で衚瀺したい情報ずそうでもない情報があるような堎合にuseDeferredValueを䜿甚できる可胜性がありたす。

最優先で衚瀺したい情報ずいうのは、代衚的なものはナヌザヌぞのフィヌドバックです。これはuseTransitionのずきも出おきたキヌワヌドですね。特に䞊のサンプルのinput芁玠の堎合、これはcontrolled componentであるためtextずいうステヌトの倉曎がすぐに反映されないずナヌザヌが違和感を感じおしたうため、䜕を差し眮いおも最優先で再レンダリングを完遂する必芁がありたす。

䞀方で、䞋の<Show10000Times text={deferredText} />はそれよりも優先床が䜎い情報であり、input芁玠ず同レベルの軜快さは求められたせん。そのためここでtextではなくdeferredTextを枡しおいたす。これにより、textが曎新された瞬間にShow10000Timesを再レンダリングする必芁が無くなるのです。

実際にサンプルを動かしおいる方は、詊しにこれを<Show10000Times text={text} />に倉えおみたしょう。するず、input芁玠の応答性がかなり悪くなるはずです。これはtextが倉わるたびにこのコンポヌネントを再レンダリングする必芁があり、それが終わらないずinput芁玠も再レンダリングされないからです。

たずめるず、この䟋ではuseDeferredTextを甚いおShow10000Timesの再レンダリングを遅延させるこずで、input芁玠の反応速床を挙げおいるのです。

useTransitionのずきず同様に、この問題も適切なステヌト管理によっおも解決できるかもしれたせん。そんれにも関わらずReactがこの機胜を甚意した理由は、やはりレンダリングの䞀番適切なスケゞュヌリングが出来るのはReact自身だからずいうこずに尜きるでしょう。

useDeferredValueが埗意な状況ず苊手な状況

実は、䞊の䟋はサンプルずしお最善のものではありたせん。実際、input芁玠をいじっおいるずそこそこ頻繁にShow10000Timesの再レンダリングが走るたびに応答性が悪くなっおしたいたす。

これは、たったく効果が無いわけではありたせんが今回のサンプルがuseDeferredValueの苊手な状況だからです。

具䜓的には、今回応答性が悪くなる原因はshow10000TimesのレンダリングのボトルネックがDOMぞの反映だからです。ナヌザヌに䞭途半端な状態を芋せないために、いわゆる仮想DOMを実際のDOMぞず反映させる䜜業は䞀気に行う必芁がありたす。そのため、せっかくReactがレンダリングを䞭断できる機胜を実装したずいっおも、DOMぞの反映は分割できないのです。たあ、だからこそ実際のDOM操䜜を最小限にするこずが重芁になっおくるのですが。

逆に蚀えば、仮想DOMの構築段階が重い堎合単玔にツリヌが倧きいずか、関数コンポヌネントに重い凊理があるなどの堎合はuseDeferredValueの埗意な状況ずなりたす。仮想DOMレベルのレンダリングの段階ではReactがより䞊手にスケゞュヌリングを行うこずができるからです1。

useDeferredValueずuseTransitionの䜵甚

ここたでで2぀のフックを玹介したしたので、最埌に2぀を䜵甚する䟋を玹介したす。

特に、䞊のuseDeferredValueの䟋はメむンスレッドを専有するレンダリング凊理に察する察応策でしたが、実はuseDeferredValueはSuspenseず組み合わせおもいい感じに動きたす。

今回の䟋はこれです。

06-useDeferredValue-example3.tsx抜粋
export const Example06 = () => {
  const [text, setText] = useState("");
  const [dataId, setDataId] = useState(0);
  const [startTransition] = useTransition({ timeoutMs: 10000 });

  const deferredDataId = useDeferredValue(dataId, { timeoutMs: 10000 });

  return (
    <>
      <h1>06 Example of useDeferredValue 3</h1>
      <p>
        <input
          value={text}
          onChange={e => {
            const newText = e.currentTarget.value;
            setText(newText);
            startTransition(() => {
              setDataId(newText.length);
            });
          }}
        />
      </p>
      <Suspense fallback={<p>Loading...</p>}>
        <p>デヌタID: {dataId}</p>
        <LoadedContents dataId={deferredDataId} />
      </Suspense>
    </>
  );
};

const LoadedContents: React.FC<{ dataId: number }> = ({ dataId }) => {
  const data = getData(dataId);
  return <p>{data}</p>;
};

たたSuspenseの話なので、デヌタをロヌドしお衚瀺するコンポヌネントLoadedContentsを甚意したした。デヌタを読み蟌むには時間がかかるため、このコンポヌネントはサスペンドする可胜性がありたす。

実際の動䜜ずしおは、input芁玠に入力した文字列の文字数がデヌタIDずしお衚瀺され、さらに䞀定時間埌にそのIDのデヌタが読みこたれお衚瀺されたす。useDeferredValueの効果により、デヌタIDず実際に衚瀺されるデヌタに差異が発生するこずがありたす。䞋の䟋では、デヌタIDが21なのに実際に衚瀺されおいるデヌタは17番のものです。

䟋6のスクリヌンショット

useDeferredValueずuseTransitionの䜵甚時の挙動

䞭身の解説に移りたす。メむンのコンポヌネントExample06のステヌトは二぀で、ひず぀はuseDeferredValueの䟋ず同じくtext、もうひず぀はdataIdです。dataIdは読み蟌みたいデヌタの番号です。dataIdはtextの文字数ずしたす。

それならわざわざdataIdずいうステヌトを甚意しなくおもconst dataId = text.length;でいいような気がしないでもないですが、今回そうできない理由がありたす。input芁玠のonChange属性を芋るず分かりたすが、textはstartTransitionの倖で、dataIdは䞭で倉曎しおいたす。この違いのためにステヌトを別々にする必芁があったのです。

startTransitionの倖で倉曎されおいるtextは即座に倉曎されたす。これにより、input芁玠は垞にナヌザヌの入力に察するフィヌドバックを玠早く返したす。

dataIdは<p>デヌタID: {dataId}</p>ずいうずころで䜿われおいたす。これはstartTransitionの䞭で倉曎されるため、新しいデヌタが読みこたれるたでは衚瀺が曎新されたせん  ずいいたいずころですが、実はそうでもありたせん。䞊蚘のように、新しいデヌタが読みこたれる前にdataIdが曎新されるこずがありたす。

これはuseDeferredValueによるものです。LoadedContentsに枡しおいるのはdeferredDataIdであり、dataIdが曎新された盎埌はただdeferredDataIdが曎新されおいないためにサスペンドが発生したせん。これにより、即座にdataIdの曎新が画面に反映されるのです。ただ、useTransitionの効果により、䞀旊サスペンドが発生したらdataIdの曎新が画面に反映されなくなりたす。これにより、dataIdが垞にぎったりtextに远随するずいうわけでもなくなりたす。

ここで匷調したいのはuseDeferredValueの効果です。サスペンドするコンポヌネントに枡される倀を遅延させるこずで倉曎盎埌のサスペンドを防ぐこずができる可胜性があり、その結果ずしおdataIdの倉化が早めにナヌザヌに衚瀺されるのです。

今回はあたり意味がない䟋に芋えたすが、耇数の゜ヌスからデヌタを読み蟌むずきに早いほうのデヌタを読み蟌めたらすぐに衚瀺し、か぀遅いデヌタは読み蟌め次第衚瀺するそれたでは叀いデヌタを衚瀺しおおくずいうような堎合においお圹に立ちそうです。

useDeferredValueずSuspense

今回の䟋でも、useDeferredValueによるいくらかの遅延が発生しおいるこずが分かりたした。

しかし、今回は前の䟋のようにメむンスレッド的に重い凊理があるわけではありたせん。代わりにあるのはサスペンドするコンポヌネントです。

このこずから、サスペンドするコンポヌネントもuseDeferredValueに察するスケゞュヌリングに圱響を䞎えるこずが分かりたす。

正盎に述べるず、どんな時にどれくらい遅延するのかはよく分かりたせんでした。䟋えば公匏のサンプルではサスペンドしおいたコンポヌネントが䞀぀レンダリング完了した時点で遅延曎新が行われる挙動が確認できたすが、なぜそのタむミングなのかは非自明です。恐らく、React偎ずしおもここはヒュヌリスティクスの入る䜙地がある郚分なので、あたり厳しい制玄をかけたくないのであろうず思いたす。

今のずころ唯䞀信頌できる性質は、元の倀が倉化した瞬間はuseDeferredValueの返り倀はそれに远随しないずいうものだず思われたす。぀たり、必ず元の倀useDeferredValueの匕数に枡された倀よりも叀い倀が返される瞬間が存圚するようです。

個人的には、正匏版リリヌスたでにはもう少し分かりやすくなっおほしいなあず思わないでもありたせん。䜿い続けおいればそのうち理解できるかもしれたせんので、次の蚘事を期埅せずにお埅ちください。

所感

たず最初にPromiseをthrowするこずに぀いお述べおおくず、なかなか面癜いですね。非同期凊理ずいうのはどこか䞀箇所でも混ざりこむず凊理の党䜓を非同期に察応するように曞かないずいけないわけですが、その察応をReactに䞞投げ文字通りするこずで匷匕に解決し、ナヌザヌランドではたるで同期凊理のように芋える凊理を曞くこずができたす関数型蚀語のナヌザヌたちは副䜜甚の皮類が倉わっただけじゃんず蚀うかもしれたせんが。それが“良い”かどうかはさおおき、React的な発想であるず感じられたした。

さお、この蚘事では2぀の新しいフックを玹介したしたが、䞡方に共通するキヌワヌドはナヌザヌ゚クスペリ゚ンスでしたね。特に、望たしいフィヌドバックを最速で返すこずに焊点が眮かれおいたす。もちろんそれは埓来から我々が心血を泚いできたものですが、今回React本䜓にそれを支揎する機胜が入ったこずになりたす。

その背景は、䞀぀はReact本䜓がやるべき仕事をReact本䜓がやるようにしたずいう単玔な話です。しかしもう䞀぀、䞭倮集暩的ではなくコンポヌネントベヌスな手法をさらに掚進しおいく意思が感じられたす。

個人的な所感ずしおは、確かにその方向性で出来るこずが広がったものの、ただ党郚把握しきれる気がしないなあず感じおいたす。色々なコンポヌネントがステヌトを持っおいるだけでも倧倉なのにそれらがサスペンドしたりしなかったりするずなるず、ロゞックの党䜓像の把握は非垞に困難になりたす。

Reactの思想ずしおは、コンポヌネントが成す朚構造を䞭心に据えるずいう点ではずおも䞀貫しおいたす。朚構造の䞊でロゞックを適切に分割しおやるこずで我々が考えなければいけないこずを枛らすずいう方向性が芋お取れたす。

ここから蚀えるこずは、Suspenseを始めずしたこれらの新機胜を䜿いこなすにはそのための粟巧な蚭蚈が必芁であろうずいうこずです。埓来䞭倮集暩的な方法で解決されおきた問題に察しおReactはコンポヌネントベヌスの新たなアプロヌチを䞎えたした。次に我々がすべきこずはこのアプロヌチを受け入れ咀嚌するこずです。

React自䜓が「Suspense゚コシステム」ず呌んでいるように、Promiseをthrowするずいった抂念は、呚蟺ラむブラリに新たな圱響を䞎えたす。これはちょうどHooksが登堎したずきのようです。

Reactはdata fetching libraryを䞻に芋おいるようですが、その流れで状態管理ラむブラリにも新しい蚭蚈のものが出るのではないかず思いたす。この蚘事を読んでconcurrent modeを完党に理解したずいう方がいれば自分で䜜っおみおもいいかもしれたせん。

たた、今回の新機胜によっおReactの非自明さが増したずいう印象を受けたした。特にuseDeferredValueの挙動においお顕著です。それに、useTransitionが絡んでくるず䜕がい぀再レンダリングされるのか正盎ただ予枬が付きたせん。䜿っおいるうちに理解できるのか、それずも理解しようずしないずずっず非自明なたたなのかはただ定かではありたせん。

先にも述べたようにReactは結果の敎合性を保ち぀぀レンダリング䞭の皮々の保蚌を枛らす方向に動いおいたす。自分はこれたでReactはフレヌムワヌクじゃなくおラむブラリだよずいう䞻匵を続けおきたしたが、䜕ずなくフレヌムワヌクっぜさが出おきたなあずいう気がしないでもありたせん。

たずめ

たず、蚘事の埌半になるに぀れお文章が読みにくくなっおいったず思いたす。最初は真面目に蚘事を曞いおいたのですが埌半疲れおきお、だんだんReactを觊っお脳内をダンプしただけみたいな文章になっおしたいたした。それでも䜕かの参考になれば幞いです。

ずいうこずで、10月25日に公開されたReact Concurrent Modeのプレビュヌ版を調査し、useTransitionずuseDeferredValueずいう2぀の新しいフックに぀いおたずめたした。

useTransitionは画面遷移前ず埌の2぀の状態をReactに同時に管理しおもらうこずができるフックでした。たた、useDeferredValueは倀がいい感じに遅延しながら倉化するずいうフックでした。

これらがどんな問題を解決するのかず蚀えば、ナヌザヌ゚クスペリ゚ンスの向䞊です。そのためにReactが䟛えおいるべき機胜ずしお䞊蚘のフックが远加されたわけです。

これが実珟するには䞭断可胜なレンダリングの機構が必芁で、それが長い時間をかけお実装されたconcurrent modeです。

所感は䞊に述べた通りですが、たずめ盎すずReactの非自明さが少し増したなずいう印象を受けたした。筆者は7〜8時間くらいはReactのconcurrent modeをいじりたしたが、ただ良くわかっおいない点も倚くありたす。

理解を深めるにはもっず䜿い倒す必芁があるず感じおいたすが、React公匏偎からのさらなる解説もできれば期埅したいずころです。


  1. ただし、ひず぀の関数コンポヌネントの凊理にものすごく時間がかかるずいうような状況には無力です。Reactでもさすがに関数の実行を䞭断させるこずはできないからです。それよりも、たくさんの関数コンポヌネントがあっお党郚凊理するず思いずいうような状況に察しおより有効です。 â†©

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away