📚

React18の新Hooks -「useTransition」の使い方📚

2022/04/02に公開

投稿する背景

本記事で2回目の投稿になります!💡

先日ReactがメジャーアップデートしてReact18が遂にお披露目となりました👏
ここ数日Twitterの話題がReact18で持ちきりなのですが、果たして一体どのようなアップデートがされたのか、傍観者でなく実際にコードを触ってみて筆者自信「感動した!!😭」ので、共有したいと思い執筆しております。
(進撃の巨人で言うと、「アルミン・アルレルト」が初めて「海」を見て感動している感じですね。見たいと願うだけでなく、しっかり行動することで「海」にたどり着いていますよね😆プログラミングも傍観者にとどまらずに、実際にローカルで挙動を確認することは大切ですね!笑)

そんな中、触る中で感動した新しいHooksの1つである「useTransition」の使い方について、サンプルコードを挟みながら本記事を通して解説していきたいと思います💨

対象読者

  • とりあえずReact18の新しいHooksの「useTransition」の使い方を知りたい方⚡️
  • Hooksを使って、少しでもユーザーエクスペリエンス(以下、UXと略す)を向上させたい方👀

読了後のゴール設定

  • 「useTransition」を用いて、緊急的な状態の更新か緊急ではない更新かを制御して、UXを向上することができていること💡
  • なんとなくでいいので、 「useTransition」の使い方を理解していること💡

なお、本記事は以下のYouTubeを参考にしております。

https://youtu.be/lDukIAymutM」

結論

data.js
export const generateProducts = () => {
  // NOTE: 1万のダミーデータを作成
  const products = [];
  for (let i = 0; i < 10000; i++) {
    products.push(`Product ${i + 1}`);
  }
  return products;
};

こちら(data.js)で1万のダミーデータを作成するための関数。

ProductList.jsx
import React from 'react';

const ProductList = ({ products }) => {
  return (
    <ul>
      {products.map((product, index) => (
        <li key={index}>{product}</li>
      ))}
    </ul>
  );
};

export default ProductList;

こちら(ProductList.jsx)で作成された1万のデータを表示するためのコンポーネント。

UseTransition.jsx
import React, { useState, useTransition } from 'react';
import ProductList from './ProductList';
import { generateProducts } from './data';
import './index.css';

// NOTE: 1万のダミーデータを作成
const dummyProducts = generateProducts();

const filterProducts = (filterWord) => {
  // NOTE: 抽出キーワードがなければ、1万のデータを返す。
  if (!filterWord) {
    return dummyProducts;
  }
  // NOTE: 抽出キーワードでフィルタリングを行う。
  return dummyProducts.filter((product) => product.includes(filterWord));
};

const UseTransition = () => {
  const [isPending, startTransition] = useTransition();
  const [filterWord, setFilterWord] = useState('');

  // NOTE: 特定のワードをキーに、1万のダミーデータから対象データを抽出
  const filteredProducts = filterProducts(filterWord);

  const updateFilterHandler = (event) => {
    // NOTE: 状態(ステート)の更新を遅らせて、ユーザーのインプットの入出力を優先させる。(状態更新の優先順位低)
    startTransition(() => {
      setFilterWord(event.target.value);
    });
  };

  return (
    <div id='app'>
      <h1>useTransition</h1>
      <input
        type='text'
        placeholder='数字を入力してください'
        onChange={updateFilterHandler}
      />
      <p>
        {/* NOTE: 状態(ステート)の更新を遅らせている間は、「isPending = true」となる。 */}
        {isPending && (
          <span style={{ color: 'white' }}>
            プロダクトをアップデート中・・・
          </span>
        )}
      </p>
      <ProductList products={filteredProducts} />
    </div>
  );
};

export default UseTransition;

サンドボックスで確認

結論を述べましたが、結局自分で体感する方が早いと思うので、サンドボックスを用意しました。

①「useTransition」を使用せずに「useState」のset関数で状態を更新

1つ目は、「useTransition」を使用せずに「useState」のset関数で状態を更新している例。
つまり、状態(ステート)はユーザー操作に応じてリアルタイムで変動して画面に再レンダリングされるので、ユーザーが入力したキーワードとそれに当てはまるデータが常に一致している表示されている状態となります。

UseTransition.jsx
  const updateFilterHandler = (event) => {
-   // NOTE: 状態(ステート)の更新を遅らせて、ユーザーのインプットの入出力を優先させる。(状態更新の優先順位低)
-   startTransition(() => {
      setFilterWord(event.target.value);
-   });
  };

例1)従来通りuseStateのset関数で更新した場合

実際に、連続で「1234」を入力したり、その状態から「Deleteキー」を長押しして削除してみるとわかるのですが、インプットテキストに入力する体験(インタラクティブ性)がかなり悪いことがわかるかと思います・・・😅
(特に、削除する時に一瞬数字がフリーズするのではないでしょうか??)
React側が
「入力されたキーワードと表示されているリストに不整合があってはダメだから、1つずつ処理するからちょっと待ってな!!」
のような感じで、内部計算する間、インプットテキストが一瞬フリーズしたように感じられ、UXがかなり酷いです。
つまり、入力されたキーワードと表示されているリストのデータの整合性が取れるよう画面の更新を同期的に行なっていることになります。

②React18の新しいHooksである「useTransition」で「useState」のset関数をラップして状態を更新

2つ目は、 React18の新しいHooksである「useTransition」で「useState」のset関数をラップして状態を更新する例。
こうすることで、「ラップされたセット関数は優先順位低いから後で処理するわ、先にユーザーが入力した値を画面に反映させておいて!!」のように React側に伝えることができます。
つまり、状態(ステート)の更新の優先度は低くなり、内部計算が終わる前に、インプットテキストの更新が反映されるので、ユーザーが入力したキーワードとそれに当てはまるデータが常に一致しているとは限らないこととなります。

不整合って不味くないか??
誤ってユーザーが解釈してしまったらどうするんだ!!😡

のように思われる方がいるかと思いますが、そこの対応も React18の「useTransition」で用意されているので、心配ございません😌
後ほど解説します💡

UseTransition.jsx
  const updateFilterHandler = (event) => {
+   // NOTE: 状態(ステート)の更新を遅らせて、ユーザーのインプットの入出力を優先させる。(状態更新の優先順位低)
+   startTransition(() => {
      setFilterWord(event.target.value);
+   });
  };

例2)React18の「useTransition」でset関数をラップして更新した場合

こちらも実際に、連続で「1234」を入力したり、その状態から「Deleteキー」を長押しして、入力や削除してみるとわかるのですが、インプットテキストに入力する体験がuseStateで更新する時よりもかなり改善されていることがわかります💡
(特に、削除する時に数字がフリーズせずに削除できているのではないでしょうか??)
React側が
「入力されたキーワードと表示されているリストに不整合があってもいから、UXだけは良くしてくれ!!」
のような感じで、内部計算する間も、常にユーザーの入力が滞らないように配慮がされていることが分かります。
つまり、入力されたキーワードと表示されているリストのデータは後から整合性が取れるように非同期的に行なっていることになります。

ここで、先ほどとの違いがUX以外にもあります!!
それは、非同期的に内部計算を行なっていると間は、プロダクトをアップデート中・・・と表示されていることですね!!
これは、「useTransition」の返り値の配列0番目に格納されている、下記で言う、「isPending」に真偽値で遅延状態(内部計算処理中)の状態が示されています。

UseTransition.jsx
// NOTE: isPendingは遅延させた処理の状態を示す。遅延状態の時は「true」、そうではない場合は「false」が格納されています。
const [isPending, startTransition] = useTransition();

上記より、ユーザーには「計算途中なので、少々お待ちください」のよう遅延状態を「isPending && 〜〜」のように真偽値を活用することで、UXを改善しつつも、不整合であるか否かの状態もユーザーに見せて、伝える(対話する)ことができます。

最後に

いかがだったでしょうか??🤔

React18から登場したuseTransitionを用いれば、ユーザーインタラクティブ性が上がり、より良いUXをお届けできることは間違いないと思います!

ただ、執筆者自身、まだまだ上部の理解で止まっており、なぜこのAPIが追加されたのかなどの、ライブラリ提供者の真意がまだわかっていない状態です。

なので、タイトルで示した通り、「使い方」に限って記事を書かさせていただきました👀

僕よりもっと深い知見をお持ちのエンジニアがたくさんいるはずなので、もし、 よりRecat18の理解を促進できるような記事等がありましたら、シェアしていただけると幸いです!

また、この解説記事に誤りがある、もしくは、よりいい言葉のニュアンスや伝え方がある等がありましたら、コメントいただけると嬉しいです!!

これからも、皆さんの多大な知見を吸収しながら、強強エンジニアへと1歩ずつステップアップしていけたらと思っています🙇‍♂️

ここまで読んでいただき、ありがとうございました!😄

Discussion