(1番の仕事道具)Reactの理解を深めました。

Posted date at 2024-05-28

React

Udemy

 Reactはもうすぐv19がリリースされると言われています。ReactにはHooksという概念があり、v18にもHooksがたくさんあります。v19を迎えるにあたり、普段使用している基本的なHooksの使用方法の確認とパフォーマンスチューニングにつながるHooksについて深く学びました。

 受講の理由は、フレームワークへの理解を深めることと、パフォーマンスチューニングの手法をつかむためです。

 開発規模が大きくなり、コンポーネントの数やデータの量が増えると様々な問題が出てきます。アプリ全体で、表示がもっさりしてきたり、描画がチラついたりなどです。

 他にも、例えば、キー入力が遅延する。ボタンを押して画面を更新するときに描画がもっさりする。チェックボックスにチェックをいれようとしたときチェックが入るのに時間がかかる、ページ表示の時に描画がちらつくなどです。


🚀受講した講座

🐡【完全保存版】React Hooksを完全に理解するHooksマスター講座【React18~19対応】

講師 ShinCode さん

動画再生4.0時間  林がかけた時間10時間

説明文

 


 

🐡今後のフロントエンド開発で必須知識となるReact v18の機能を丁寧に理解する

講師 じゃけぇ さん

動画再生4.0時間  林がかけた時間10時間

説明文

 


🚀v18以降でリリースされたHooks

🐡useTransition

 このHooksで用意されている関数でセット関数を囲むと、対象のステート(変数)の変更を遅延させることができる。

例えば以下の例は1万件の「Item x」というリストを表示し、テキスト入力があるごとにそのテキストでフィルタリングする実装である。useTransitionを使用しないと、キー入力からその結果がテキストボックスに反映されるまでに、最大1万件のデータの採描画が実行されるため、ユーザはテキスト入力がもっさりすると感じる可能性がある。

 useTransitonで提供されるstartTransitionを使用するとリストの更新がReact側で遅延されるため、テキストの変更はリストの変更を待たずに反映される。

 〇該当箇所

startTransition(() => { const filteredList = data.filter((item) => item.toLowerCase().includes(value.toLowerCase()) ); setList(filteredList); });

 〇全体

import React, { useState, useTransition } from 'react'; const data = Array.from({ length: 10000 }, (_, index) => `Item ${index + 1}`); const App = () => { const [inputValue, setInputValue] = useState(''); const [list, setList] = useState(data); const [isPending, startTransition] = useTransition(); const handleChange = (e) => { const value = e.target.value; setInputValue(value); startTransition(() => { const filteredList = data.filter((item) => item.toLowerCase().includes(value.toLowerCase()) ); setList(filteredList); }); }; return ( <div> <input type="text" value={inputValue} onChange={handleChange} placeholder="Filter items" /> {isPending && <p>Updating list...</p>} <ul> {list.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> </div> ); }; export default App;

 


🐡useDeferredValue

 このHooksでステートを囲むことにより生成されたステートはデフォルトで遅延する属性を持つ。先に説明した、useTransitionはセット関数を遅延させるときに使用し、こちらはステートを遅延させるときに使用する。

 以下の例は、テキストボックスに値を入力すると200個の要素「Text:xxxx」のxxxxの部分が連動して変わるものである(なお、サンプルのためわざと遅延も実装している)。useDeferredValueを使用しないと、テキストを1文字入れるたびに画面が固まる。以下の例では、親要素でuseDeferredValueを使用して生成したステートであるdeferredTextを子要素に渡すことで、子要素のリストの変更を遅延させている。

 動作としては、テキストを何文字か入力すると遅れてリストが更新されるという動きをする。これによりユーザの操作のをブロッキングを回避できる。

 〇該当箇所

const deferredText = useDeferredValue(text); <SlowList text={deferredText} />

 〇親要素

import { useDeferredValue, useState } from "react"; import SlowList from "./SlowList"; const Lesson8_2 = () => { const [text, setText] = useState(""); const deferredText = useDeferredValue(text); return ( <div> <input type="text" onChange={(e) => setText(e.target.value)} className={`border-2 border-slate-400 px-3 py-3 rounded-md`} value={text} /> <SlowList text={deferredText} /> </div> ); }; export default Lesson8_2;

〇子要素

const SlowList = function SlowList({ text }: { text: string }) { // Log once. The actual slowdown is inside SlowItem. console.log("[ARTIFICIALLY SLOW] Rendering 200 <SlowItem />"); const items = []; for (let i = 0; i < 200; i++) { items.push(<SlowItem key={i} text={text} />); } return <ul className="items">{items}</ul>; }; function SlowItem({ text }: { text: string }) { const startTime = performance.now(); while (performance.now() - startTime < 1) { // Do nothing for 1 ms per item to emulate extremely slow code } return <li className="item">Text: {text}</li>; } export default SlowList;

🐡Suspense(の表向きの姿)

 ローディングの表示を宣言的に行うことができる。例えばコンポーネント単位での部分的なローディング表示を簡潔に実装できる。ローディング時のコンポーネントの差し替えを簡素に実装できるので、例えば以下のようなスケルトンローディングを実装しやすい。ちなみに林は、背景を透過したローディングスピナーをよく使用しています。

scelton.png

 

 以下の例では、子要素(Albums)でアーティストのアルバムデータをフェッチしている間、ローディングコンポーネント(AlbumGlimmer)を代わりに表示する例である。

 〇該当箇所

<Suspense fallback={<AlbumsGlimmer />}> <Panel> <Albums artistId={artist.id} /> </Panel> </Suspense>

 〇親要素

/* eslint-disable @typescript-eslint/no-explicit-any */ import { Suspense } from "react"; import Albums from "./Albums.js"; import Biography from "./Biography.js"; import Panel from "./Panel.js"; export default function ArtistPage({ artist }: any) { return ( <> <h1>{artist.name}</h1> <Biography artistId={artist.id} /> <Suspense fallback={<AlbumsGlimmer />}> <Panel> <Albums artistId={artist.id} /> </Panel> </Suspense> </> ); } function AlbumsGlimmer() { return ( <div className="bg-slate-300"> <p>Loading...</p> </div> ); }

 〇子要素(アルバムリストのフェッチ(疑似データを生成))

/* eslint-disable @typescript-eslint/no-explicit-any */ import { fetchData } from "./data.js"; export default function Albums({ artistId }: { artistId: string }) { const albums = use(fetchData(`/${artistId}/albums`)); return ( <ul className="border-2 px-2 py-3 border-orange-300"> {albums.map((album: any) => ( <li key={album.id}> {album.title} ({album.year}) </li> ))} </ul> ); } // This is a workaround for a bug to get the demo running. // TODO: replace with real implementation when the bug is fixed. function use(promise: any) { if (promise.status === "fulfilled") { return promise.value; } else if (promise.status === "rejected") { throw promise.reason; } else if (promise.status === "pending") { throw promise; } else { promise.status = "pending"; promise.then( (result: any) => { promise.status = "fulfilled"; promise.value = result; }, (reason: any) => { promise.status = "rejected"; promise.reason = reason; } ); throw promise; } }

🐡Suspense(の本質)

 Suspenseで囲った要素は、SSR(ServerSideRendering)となる(すごい)。つまり、見た目上、ローディング中と表示されている部分の裏側にあるコンポーネントは自動的にサーバサイドでHtmlが生成されて返される(これは、SSRが標準のNext.jsの思想とリンクしています)。この、サーバサイドでHtmlを部分的に生成してブラウザに返す仕組みを「StreamingHTML」という。

 また、HtmlとJavascriptの紐づけをする操作をハイドレーションといい、Suspenseで囲ったコンポーネントについてはこのハイドレーションもサーバサイドで行われるようになる。さらに、Suspenseで囲ったコンポーネントについては、ユーザのクリック操作やキーボード操作を検知して、操作が行われたコンポーネントのハイドレーションの優先順位を上げたり下げたりする。

 上記はすべてReact側が裏側で行ってくれるので、開発者はそれを意識することはない。おかげで我々は技術的に高度な課題を抱えることなく、ユーザ体験を向上させることにのみフォーカスできる。


🚀サードバーティ製のHooks

🐡useTanstackQueryとuseSWR

 APIでデータフェッチするためのパフォーマンスを最適化したHooksである。おなじみのuseEffectより高速でバックエンドへのコールをすることができ、キャッシュ機能を用いることができる。以下はuseTanstackQueryを使用して、30分(1800秒)のキャッシュを有効にした例である。製造みえる化アプリでは読み込み時に大量の受注データ、工程データをフェッチするため、このHooksを実装したいと考えている。

import { useQuery, QueryClient, QueryClientProvider } from 'react-query'; import axios from 'axios'; // QueryClientを設定 const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1800 * 1000, // キャッシュの有効期間を30分に設定(ミリ秒単位) cacheTime: 1800 * 1000, // キャッシュがGCされるまでの期間(ミリ秒単位) }, }, }); const fetchData = async () => { const { data } = await axios.get('https://api.example.com/data'); return data; }; const MyComponent = () => { const { data, error, isLoading } = useQuery('myData', fetchData); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return <div>{JSON.stringify(data)}</div>; }; const App = () => ( <QueryClientProvider client={queryClient}> <MyComponent /> </QueryClientProvider> ); export default App;

🚀まとめ

 これまでは、Reactv18を使用しつつも、v18以前から存在していた、useState、useEffect、useRefを9割、それ以外のサードバーティ製のHooks(MUI、TanstacTable、ReactHookFormなど)を1割のような感じで開発をしていました。フレームワークのおかげでそれなりに動くアプリは作れましたが、動作上気がかりな点が多いのも事実でした。

 今回学んだHooksを使用して改善できそうな箇所がいくつかあるので、要望対応する際に、併せてリファクタリングしたいと思います。

 フロントエンド開発はユーザ体験に直接かかわる部分を担うため各種フレームワークはパフォーマンスを追及しています。React(Next.js)においては、ブラウザレンダリング(CSR)とサーバサイドレンダリング(SSR)の使い分ができるかどうかがエンジニアの腕にかかっていると感じました。例えば、ユーザの操作により表示が頻繁に変化するコンポーネントはCSR、データを読み込んでから表示するなど描画に時間がかかるものはSSRにするなどです。

←ホームに戻る