最近reactのhooksを勉強しました。実務でよく使うuseStateとuseEffect以外のhooksも勉強して「便利だな~~」と思いました。
今回はその中の一つ、「useTransition」について復習がてらまとめてみました。よろしくお願いいたします。
まず、普通にタブのコンポーネントを実装します。このうち、タブ2はわざと非常に重い処理で表示までに時間がかかるようなものにしています。
import { memo, useState } from "react"; import styles from "./Content1.module.css"; const tabTitles = ["tab1", "tab2", "tab3"]; export const Content1 = () => { const [activeTab, setActiveTab] = useState("tab1"); const handleClick = (title: string) => { setActiveTab(title); }; return ( <div className={styles.container}> <TabHeader titles={tabTitles} activeTab={activeTab} handleClick={handleClick} /> <div className={styles.content}> {activeTab === "tab1" && <Tab1 />} {activeTab === "tab2" && <Tab2 />} {activeTab === "tab3" && <Tab3 />} </div> </div> ); }; type TabContentProps = { titles: string[]; activeTab: string; handleClick: (title: string) => void; }; const TabHeader = ({ titles, activeTab, handleClick }: TabContentProps) => { return ( <div className={styles.tabHeader}> {titles.map((title) => { return ( <div key={title} className={`${styles.tabButton} ${activeTab === title ? styles.active : ""}`} onClick={() => handleClick(title)} > {title} </div> ); })} </div> ); }; const Tab1 = () => { return <h1>tab1</h1>; }; //表示までに時間がかかるコンポーネント const Tab2 = memo(function PostsTab() { let items = []; for (let i = 0; i < 500; i++) { items.push(<SlowPost key={i} index={i} />); } return <ul className="items">{items}</ul>; }); const SlowPost = ({ index }: { index: number }) => { let startTime = performance.now(); while (performance.now() - startTime < 1) { // Do nothing for 1 ms per item to emulate extremely slow code } return <li className="item">{index + 1}</li>; }; const Tab3 = () => { return <h1>tab3</h1>; };
このコンポーネントを動かすと、タブ1から素早くタブ2→タブ3とクリックしたとき、少し時間が経ってからタブ3が表示されます。
なんとなく変な感じがしますね。これにuseTransition
を使ってみます。
(実装はReact公式を参考にしました。)
useTransition
英語に弱いので翻訳すると「transition」は「移行、遷移、変遷」だそうなので、つまりは状態が変わるときに使用できそうなhooksですね。
react公式の説明を引用すると
import { useTransition } from 'react'; function TabContainer() { const [isPending, startTransition] = useTransition(); // ... }返り値
useTransition は常に 2 つの要素を含む配列を返します。トランジションが保留中であるかどうかを示す isPending フラグ。
更新をトランジションとしてマークするための startTransition 関数。
ということですが、
説明だけではぱっとイメージが付きづらいので、先ほど作ったコンポーネントで試してみます。
まずコンポーネントのトップでuseTransitionを呼び出します。そしてタブをクリックしたときのイベントハンドラhandleClick
内でstartTransition()
を使用します。
const [isPending, startTransition] = useTransition(); const handleClick = (title: string) => { startTransition(() => { setActiveTab(title); }); };
この変更を加えると、タブ1にいる状態でタブ2、タブ3と素早くクリックしたとき、素早くタブ3の内容が表示されるようになります。
useTransitionを使うと、タブ操作によって処理待ちが発生しない状態にしてくれて便利ですね。
isPendingの切り替わるタイミングですが、公式では以下のように説明されています。
トランジションの進行中状態に関するフィードバックをユーザに提供するために、startTransition が最初に呼び出されると isPending state が true に切り替わり、すべてのアクションが完了して最終的な状態がユーザに表示されるまで true のままになります。
これもちょっとピンとこないので、コンポーネントの下にisPendingを表示してみました。
処理の重いタブ2を読み込んでいる間はtrueになっているようです。説明にも「最終的な状態がユーザに表示されるまで」とあるので、setActiveTabの更新による再レンダリングが終わるまでtrueになっていると解釈できそうです。(違いましたらご指摘お願いしたいです🙇♀️)
非同期処理について
非同期処理の扱いには注意が必要みたいです。
例えば、トランジションでマークさせたい関数を以下の様に設定するとうまくいかない様です。
startTransition(() => { setTimeout(() => { setActiveTab(title) }, 1000); });
かわりに
setTimeout(() => { startTransition(() => { setActiveTab(title) }); }, 1000);
これだとトランジションがマークされます。startTransitionの中身は同期処理である必要があります。
また、async/awaitを使用する場合、startTransition 関数内のawait の後に行われる state 更新はトランジションとしてマークされません。各 await 後の state 更新をそれぞれ startTransition 呼び出しでラップする必要があります。
startTransition(async () => { await someAsyncFunction(); startTransition(() => { setActiveTab(title) }); });
さいごに
便利なhooksは使ってみたいな~とは思いますが、実際に使うときは副作用とか気にしないといけない気がしてなかなか手が出せません。もっと勉強してチャレンジできるようにしたいと思います。
最後になりますが、現在弊社ではエンジニアを募集しています。
この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!