Reactを使うならReact Developer Toolsの再レンダリング時ハイライトくらい設定してくれ
モダンフロントエンドについて初めて書きます。お手柔らかに。
最近 React と Next.js に入門したのですが、入門時点で一番最初に知っておきたかったことについて書きました。
「React 初心者が useState とかを学習する前にまず一番にやることはこれ」っていう内容です。。
タイトルは自分への戒めです。
TL;DR
この記事を読むと React Developer Tools の簡単な使い方を知り、useState の再レンダリングについて動きがイメージできるようになると思います
React Developer Tools
これのこと。React を使った開発をするのであれば、必ず導入しないといけないレベルのもの。
再レンダリング時ハイライトの設定
React Developer Tools をインストールした後、F12 を押下して Component を選択この歯車を押下する。
すると、以下のような部分があると思うのでチェック ON にしてください。
一般的な使い方
こんな感じでコンポーネントの state を確認したり、レンダリング順を見たりできます。
useState を利用した場合の再レンダリング
では、以下のようなコードがあるとします。
import HeadComp from '@/Components/head'
import Header from '@/Components/header'
import RepositoryCard from '@/Components/repositoryCard'
import { GitHubRepository, getRepositories } from "./api/githubApi"
import { useState } from "react"
import Select from "@/Components/select"
type Props = {
repositories: GitHubRepository[]
language: string
sort: string
}
const languageOprionts = [
{ label: "JavaScript", value: "javascript" },
{ label: "TypeScript", value: "typescript" },
{ label: "Python", value: "python" },
{ label: "Go", value: "go" },
{ label: "Java", value: "java" },
{ label: "Kotlin", value: "kotlin" },
{ label: "Rust", value: "rust" },
{ label: "Ruby", value: "ruby" },
{ label: "PHP", value: "php" },
{ label: "Perl", value: "perl" },
{ label: "Swift", value: "swift" },
{ label: "C", value: "c" },
{ label: "C#", value: "c#" },
{ label: "C++", value: "c++" },
{ label: "Vue", value: "Vue" },
]
const sortOptions = [
{ label: "Stars", value: "stars" },
{ label: "Forks", value: "forks" },
]
export default function Home({ repositories, language, sort }: Props) {
const [languageVal, setLanguageVal] = useState(language)
const [sortVal, setSortVal] = useState(sort)
const handleLanguageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setLanguageVal(event.target.value)
}
const handleSortChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSortVal(event.target.value)
}
return (
<>
<HeadComp />
<Header />
<main>
<div className="flex justify-center">
<form method="get" action="/">
<div className="mt-4 flex flex-row">
<div className="mr-4">
<Select name="language" defaultVal={languageVal} options={languageOprionts} onChangeHandle={handleLanguageChange} />
</div>
<div className="mr-4">
<Select name="sort" defaultVal={sortVal} options={sortOptions} onChangeHandle={handleSortChange} />
</div>
<div>
<button type="submit" className="border border-black text-black font-bold py-2 px-4 rounded-full">
<i className="ri-search-line"></i>
</button>
</div>
</div>
</form>
</div>
<div className="p-4 justify-center">
{repositories.length > 0 ? repositories.map((repository) => <RepositoryCard key={repository.id} repository={repository} />) : <h1>no data</h1>}
</div>
</main>
</>
)
}
export async function getServerSideProps(context: any) {
const searchCondition = {
language: 'javascript',
sort: 'stars'
}
let language = "javascript"
let sort = "stars"
if (Object.keys(context.query).length > 0) {
if (context.query.language !== '') {
searchCondition.language = context.query.language
language = languageOprionts.find((option) => option.value === context.query.language)?.value ?? "javascript"
}
if (context.query.sort !== '') {
searchCondition.sort = context.query.sort
sort = sortOptions.find((option) => option.value === context.query.sort)?.value ?? "stars"
}
}
const repositories = await getRepositories(searchCondition)
return {
props: {
repositories,
language,
sort
}
}
}
画面の見え方としてはこんな感じです。
ではこのコンボボックスを変更した際、どこが再レンダリングされるかを確認したいと思います。
どうでしょう、ページ全体がレンダリングされてしまっていることがわかりますよね?
これはindex.tsx
で定義した useState の値がSelect
コンポーネント内部で変更されたため、親コンポーネントにあたるindex.tsx
に副作用が出ている状態です。
このように useState の値が変更されると、定義元のコンポーネントにまで遡って再レンダリングが走ります。
React Developer Tools の再レンダリング設定を ON にしていないと、console.log で確認する他ないのですがやってられないです。
また console.log で確認する方法はある程度 useState の副作用の影響範囲をしっかりイメージできている必要があるのですが、初心者がそんなことわかるよしもないので、このように視覚的に再レンダリングに気付けるようにしておきたいです。
再レンダリングされないようリファクタ
本筋で話したかったことは終わったのですが、先に見たように画面全体がレンダリングされてる状態を改善していきます。
それにあたって使われるテクニックにMemo 化などがありますが今回は state の持ち方を変えれば解決できそうです。
ついでに state の持ち方を変えながら、どこにどう定義すると再レンダリングが走るのかを見ながら直していきたいと思います。
では 1 度目の修正。
RepositorySearchForm
という子コンポーネントに state を切り出します。
import HeadComp from '@/Components/head'
import Header from '@/Components/header'
import RepositoryCard from '@/Components/repositoryCard'
import { GitHubRepository, getRepositories } from "./api/githubApi"
import { languageOprionts, sortOptions } from "@/Components/repositorySearchForm"
import RepositorySearchForm from "@/Components/repositorySearchForm"
type Props = {
repositories: GitHubRepository[]
language: string
sort: string
}
export default function Home({ repositories, language, sort }: Props) {
return (
<>
<HeadComp />
<Header />
<main>
<div className="flex justify-center">
<RepositorySearchForm language={language} sort={sort} />
</div>
<div className="p-4 justify-center">
{repositories.length > 0 ? repositories.map((repository) => <RepositoryCard key={repository.id} repository={repository} />) : <h1>NO Data or API Error. Please wait </h1>}
</div>
</main>
</>
)
}
export async function getServerSideProps(context: any) {
const searchCondition = {
language: 'javascript',
sort: 'stars'
}
let language = "javascript"
let sort = "stars"
if (Object.keys(context.query).length > 0) {
if (context.query.language !== '') {
searchCondition.language = context.query.language
language = languageOprionts.find((option) => option.value === context.query.language)?.value ?? "javascript"
}
if (context.query.sort !== '') {
searchCondition.sort = context.query.sort
sort = sortOptions.find((option) => option.value === context.query.sort)?.value ?? "stars"
}
}
const repositories = await getRepositories(searchCondition)
return {
props: {
repositories,
language,
sort
}
}
}
import { useState } from "react"
import Select from "@/Components/select"
export const languageOprionts = [
{ label: "JavaScript", value: "javascript" },
{ label: "TypeScript", value: "typescript" },
{ label: "Python", value: "python" },
{ label: "Go", value: "go" },
{ label: "Java", value: "java" },
{ label: "Kotlin", value: "kotlin" },
{ label: "Rust", value: "rust" },
{ label: "Ruby", value: "ruby" },
{ label: "PHP", value: "php" },
{ label: "Perl", value: "perl" },
{ label: "Swift", value: "swift" },
{ label: "C", value: "c" },
{ label: "C#", value: "c#" },
{ label: "C++", value: "c++" },
{ label: "Vue", value: "Vue" },
]
export const sortOptions = [
{ label: "Stars", value: "stars" },
{ label: "Forks", value: "forks" },
]
type Props = {
language: string,
sort: string
}
export default function RepositorySearchForm({ language, sort }: Props) {
const [languageVal, setLanguageVal] = useState(language)
const [sortVal, setSortVal] = useState(sort)
const handleLanguageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setLanguageVal(event.target.value)
}
const handleSortChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSortVal(event.target.value)
}
return (
<form method="get" action="/">
<div className="mt-4 flex flex-row">
<div className="mr-4">
<Select name="language" defaultVal={languageVal} options={languageOprionts} onChangeHandle={handleLanguageChange} />
</div>
<div className="mr-4">
<Select name="sort" defaultVal={sortVal} options={sortOptions} onChangeHandle={handleSortChange} />
</div>
<div>
<button type="submit" className="border border-black text-black font-bold py-2 px-4 rounded-full">
<i className="ri-search-line"></i>
</button>
</div>
</div>
</form>
)
}
すると、今度はページ全体ではなく、RepositorySearchForm
コンポーネント内だけが再レンダリングされていることがわかります。
しかし、相変わらず左のコンポーネントだけを変更しているのにRepositorySearchForm
コンポーネント全体が再レンダリングされてしまっているので、もう少し改善が必要です。
…というわけで 2 度目の修正。
今度は、Select
コンポーネント内に state を持つようにします。
import Select from "@/Components/select"
export const languageOprionts = [
{ label: "JavaScript", value: "javascript" },
{ label: "TypeScript", value: "typescript" },
{ label: "Python", value: "python" },
{ label: "Go", value: "go" },
{ label: "Java", value: "java" },
{ label: "Kotlin", value: "kotlin" },
{ label: "Rust", value: "rust" },
{ label: "Ruby", value: "ruby" },
{ label: "PHP", value: "php" },
{ label: "Perl", value: "perl" },
{ label: "Swift", value: "swift" },
{ label: "C", value: "c" },
{ label: "C#", value: "c#" },
{ label: "C++", value: "c++" },
{ label: "Vue", value: "Vue" },
]
export const sortOptions = [
{ label: "Stars", value: "stars" },
{ label: "Forks", value: "forks" },
]
type Props = {
language: string,
sort: string
}
export default function RepositorySearchForm({ language, sort }: Props) {
return (
<form method="get" action="/">
<div className="mt-4 flex flex-row">
<div className="mr-4">
<Select name="language" defaultVal={language} options={languageOprionts} />
</div>
<div className="mr-4">
<Select name="sort" defaultVal={sort} options={sortOptions} />
</div>
<div>
<button type="submit" className="border border-black text-black font-bold py-2 px-4 rounded-full">
<i className="ri-search-line"></i>
</button>
</div>
</div>
</form>
)
}
import { useState } from "react"
import React from "react"
type Option = {
label: string,
value: string
}
type Props = {
defaultVal: string,
name: string,
options: Option[],
}
export default function Select({ name, options, defaultVal }: Props) {
const [selected, setSelected] = useState(defaultVal)
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSelected(event.target.value)
}
return (
<select name={name} defaultValue={defaultVal} className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" onChange={handleChange}>
{options.map((option) => <option key={option.value} value={option.value} >{option.label}</option>)}
</select>
)
}
LGTM!
state を持ったコンポーネントだけが再レンダリングされるようになりました。
これで React の仮想 DOM 本来の思想通り、変更のあった部分のみがレンダリングされているのでパフォーマンスが発揮できていそうです。
おわりに
React Developer Tools が入ってないと、この辺りのことを見落としながら開発していって、気づけばパフォーマンスがめちゃ悪くなってるということになります。
初心者は絶対設定しておきましょう。
メンバー募集中!
サーバーサイド Kotlin コミュニティを作りました!
Kotlin ユーザーはぜひご参加ください!!
また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。
よろしければ Conpass からメンバー登録よろしくお願いいたします。
Discussion