優先度の低い更新を後回しにできるTransition
Transitionは、優先度の低い画面更新を後回しにできる機能です。この機能を、図6のサンプルで説明します。このサンプルでは、テキストボックスに入力した文字を複数回、画面に表示します。また、特定のスマホ名を入力すると、会社名を前につけて表示します。
テキストボックスの文字を連続的に更新すると「更新中...…」が表示されて画面への反映が遅れ、その後まもなく反映されることがわかります。
このサンプルで行っているTransitionの処理は、リスト5の通りです。
// state ...(1) const [inputText, setInputText] = useState(''); const [dispText, setDispText] = useState(''); // useTransitionでisPendingとstartTransactionを取得 ...(2) const [isPending, startTransition] = useTransition(); // テキストボックス文字列変更時の処理 ...(3) function onChangeInput(e) { // テキストボックスへの文字反映のため、setInputTextはすぐに行う ...(4) setInputText(e.target.value); // 優先度を低くする処理はstartTransitionで囲む ...(5) startTransition(() => { // 入力された文字列に対応した接頭辞を付与してdispTextに設定 ...(6) const prefix = getPrefix(e.target.value); setDispText(prefix + e.target.value); }); }
(1)で、テキストボックスの文字列inputTextと、画面に表示する文字列dispTextの2つのstateを定義しています。(2)でuseTransitionフックを実行して、Transition中を表すisPending変数と、Transitionを開始するstartTransition関数を取得します。画面表示では、isPendingがtrueの時に「更新中...」を表示します(詳細はサンプルコード参照)。
(3)はテキストボックスの文字列が変更されたときの処理です。テキストボックスの文字列に対応するinputTextは、入力結果をすぐに反映するため、そのままsetInputTextメソッドを実行します(4)。
一方、入力された文字列に接頭辞をつけて表示するdispTextの処理(6)は、(5)の通りstartTransitionで囲んで、優先順位が低いものと指示します。この記述により、テキストボックスの文字列が短時間に多数変更されたときには、dispTextの表示更新が遅れる代わりに、inputTextの表示は遅れない(つまりテキストボックスへの入力が遅延しない)ようになります。
優先順位の低い状態変数を指定できるuseDeferredValue
startTransitionは、囲まれた処理に対して低優先度を指定しますが、処理の記述や処理中の判定(isPending)が必要ない場合は、状態変数そのものに対して低優先度を指定するuseDefferedValueフックが利用できます(リスト6)。
const [inputText, setInputText] = useState(''); // inputTextから、優先度が低いdispTextを生成 ...(1) const dispText = useDeferredValue(inputText);
(1)の通りuseDeferredValueメソッドにinputTextを渡して生成したdispTextは、表示の優先順位が低いものと判定され、Transition同様、inputTextの表示(=テキストボックスへの文字入力)を邪魔しないよう、遅れて更新されます。実装の詳細はサンプルコード(p003-deferred)を参照してください。
サーバーサイドレンダリングの初期化処理の変更
これまで説明してきた例では、Webページはクライアント側の処理で表示されます。このような処理をクライアントサイドレンダリング(CSR)と呼びます。一方、Reactにはサーバー側でWebページのコンテンツを生成するサーバーサイドレンダリング(SSR)の機能も備えており、React 18で変更が加えられました。
図8のページをCSRとSSRで実装したサンプルで、React自身のCSRとSSRの機能を説明します。なお、現実的にはReact単体でSSRを実装することはほぼなく、Next.jsのような、Reactを利用するフレームワークを用いてSSRを実装することが多いですが、図8のサンプルではReact自身のSSR機能を説明するため、フレームワークに頼らずSSRを実装しています。
画面に表示するAppコンポーネントは、リスト7の通りです。(1)で、useStateフックでcount変数とsetCount関数を取得し、(2)でボタン押下時にcountを1増やす処理を記述しています。コンポーネントの表示内容は(3)です。
function App() { // カウント ...(1) const [count, setCount] = useState(0); // ボタン押下時の処理(カウントを1増やす) ...(2) function onClickButton() { setCount(c => c + 1); } // コンポーネントのHTMLをJSXで記述して返却 ...(3) return ( <div> <h3>Reactカウントアップアプリ</h3> <div>現在のカウント:{count}</div> <button onClick={onClickButton}>カウントアップ</button> </div> ); } export default App;
このAppコンポーネントをSSRで表示するReactの初期化処理はリスト8となります。
// React 17までの古い記述 ...(1) // import ReactDOM from 'react-dom'; // ReactDOM.hydrate(<App/>, document.getElementById('root')); // React 18からの記述 ...(2) import ReactDOMClient from 'react-dom/client'; ReactDOMClient.hydrateRoot(document.getElementById('root'), <App />);
CSR(リスト1/2)同様、SSRでも、React 17までの記述(1)とReact 18からの記述(2)が存在します。hydrate(ハイドレート)とは、SSRで生成されたDOM構造(HTML記述)に、JavaScriptの処理を後から付与して、動的なWebページとして成立させる処理です。
SSRでは、表示するHTMLをサーバー側で生成する必要があります。p005-ssrサンプルでは、この処理をserver/index.jsにリスト9の通り実装しています。Node.js用Webサーバーライブラリー「Express」を利用しています。
const PORT = process.env.PORT | 3000; const app = express(); // 「http://localhost:3000/」へアクセス時の処理 ...(1) app.get('/', (req, res) => { // HTML要素をJSXで記述 ...(2) const htmlElem = (略) <div id='root'><App /></div> (略) // HTML文字列を生成して返却 ...(3) const htmlString = ReactDOMServer.renderToString(htmlElem); res.send(htmlString); }); // buildフォルダー内のファイルにアクセスできるようにする ...(4) app.use(express.static('./build')); // Webサーバー開始 ...(5) app.listen(PORT, () => { console.log(`Server is listening on port ${PORT}`); });
Webブラウザーからアクセスされたときの処理は(1)です。ここでは(2)で記述したHTML要素を(3)でHTML文字列にしてWebブラウザーに返却します。(4)は静的ファイルの配置指定、(5)はWebサーバーを開始する記述です。(2)のHTML要素に含まれた<App />、つまりAppコンポーネントがサーバーサイドレンダリングされて、Webブラウザーに返却されるHTMLに含まれるようになります。
WebブラウザーでWebページのソースを表示させると、CSRではWebブラウザー上で動的にAppコンポーネントを描画するため、Appコンポーネントがソースに含まれません。
一方SSRでは、HTMLのソース自体にAppコンポーネントの記述が含まれています。Webページの表示時には、この記述に対してハイドレートで動的な処理が付与されます。
サーバーサイドレンダリングとサーバーコンポーネント
SSRと似た用語として、Reactには「サーバーコンポーネント」があります。SSRではWebページのHTML自体をサーバー側で生成しますが、サーバーコンポーネントではコンポーネント単位の仮想DOMまでをサーバー側で生成してクライアント側に送り、それを元にクライアント側で描画します。対して、クライアント側で仮想DOMの生成から描画までをすべて行う(今までの)コンポーネントは「クライアントコンポーネント」です。
サーバーコンポーネントの詳細については、Next.jsのドキュメントも参照してください。