⚡️

Reactを使うならReact Developer Toolsの再レンダリング時ハイライトくらい設定してくれ

2023/05/21に公開

モダンフロントエンドについて初めて書きます。お手柔らかに。

最近 React と Next.js に入門したのですが、入門時点で一番最初に知っておきたかったことについて書きました。

「React 初心者が useState とかを学習する前にまず一番にやることはこれ」っていう内容です。。

タイトルは自分への戒めです。

TL;DR

この記事を読むと React Developer Tools の簡単な使い方を知り、useState の再レンダリングについて動きがイメージできるようになると思います

React Developer Tools

これのこと。React を使った開発をするのであれば、必ず導入しないといけないレベルのもの。

https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=ja

再レンダリング時ハイライトの設定

React Developer Tools をインストールした後、F12 を押下して Component を選択この歯車を押下する。

すると、以下のような部分があると思うのでチェック ON にしてください。

一般的な使い方

こんな感じでコンポーネントの state を確認したり、レンダリング順を見たりできます。

useState を利用した場合の再レンダリング

では、以下のようなコードがあるとします。

index.tsx
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
    }
  }
}

画面の見え方としてはこんな感じです。

ではこのコンボボックスを変更した際、どこが再レンダリングされるかを確認したいと思います。

https://www.youtube.com/watch?v=PeA6lpgphjA

どうでしょう、ページ全体がレンダリングされてしまっていることがわかりますよね?

これはindex.tsxで定義した useState の値がSelectコンポーネント内部で変更されたため、親コンポーネントにあたるindex.tsxに副作用が出ている状態です。

このように useState の値が変更されると、定義元のコンポーネントにまで遡って再レンダリングが走ります。

React Developer Tools の再レンダリング設定を ON にしていないと、console.log で確認する他ないのですがやってられないです。

また console.log で確認する方法はある程度 useState の副作用の影響範囲をしっかりイメージできている必要があるのですが、初心者がそんなことわかるよしもないので、このように視覚的に再レンダリングに気付けるようにしておきたいです。

再レンダリングされないようリファクタ

本筋で話したかったことは終わったのですが、先に見たように画面全体がレンダリングされてる状態を改善していきます。

それにあたって使われるテクニックにMemo 化などがありますが今回は state の持ち方を変えれば解決できそうです。

ついでに state の持ち方を変えながら、どこにどう定義すると再レンダリングが走るのかを見ながら直していきたいと思います。

では 1 度目の修正。

RepositorySearchFormという子コンポーネントに state を切り出します。

index.tsx
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
    }
  }
}
repositorySearchForm.tsx
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>
    )
}

https://www.youtube.com/watch?v=0e4Hp5-fmJo

すると、今度はページ全体ではなく、RepositorySearchFormコンポーネント内だけが再レンダリングされていることがわかります。

しかし、相変わらず左のコンポーネントだけを変更しているのにRepositorySearchFormコンポーネント全体が再レンダリングされてしまっているので、もう少し改善が必要です。

…というわけで 2 度目の修正。

今度は、Selectコンポーネント内に state を持つようにします。

repositorySearchForm.tsx
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>
    )
}
select.tsx
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>
    )
}

https://www.youtube.com/watch?v=j7w0VFd5oOY

LGTM!

state を持ったコンポーネントだけが再レンダリングされるようになりました。

これで React の仮想 DOM 本来の思想通り、変更のあった部分のみがレンダリングされているのでパフォーマンスが発揮できていそうです。

おわりに

React Developer Tools が入ってないと、この辺りのことを見落としながら開発していって、気づけばパフォーマンスがめちゃ悪くなってるということになります。

初心者は絶対設定しておきましょう。

メンバー募集中!

サーバーサイド Kotlin コミュニティを作りました!

Kotlin ユーザーはぜひご参加ください!!

https://serverside-kt.connpass.com/

また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。

よろしければ Conpass からメンバー登録よろしくお願いいたします。

https://blessingsoftware.connpass.com/

Discussion