React customHooks を利用してロジックとUIを切り離すまでをやってみた（5/5）〜不要なレンダリングが走らないための工夫（検証 & 改善編）〜

## はじめに

React customHooks を利用してロジックとUIを切り離すまでをやってみたシリーズ

前回は、不要なレンダリングが走らないための工夫を紹介するための準備をしました。

（Reactの`memo`や`useCallback`を使った最適化を説明したいがためのコンポーネントの配備）

https://qiita.com/ryosuketter/items/548dbd70ae835fd66dfb

今回は、その実践編をやっていきます。

## これまで

Hasuraにユーザーのデータと仮定してデータを作成し

https://qiita.com/ryosuketter/items/8b01d53d0609f78c2e0e

https://qiita.com/ryosuketter/items/59a05d4eb5c715287ded

データを外部APIとして取得できるようにして、Next.js（apollo）を使って、クエリやミューテーションできるようにしました

https://qiita.com/ryosuketter/items/9e18295d78dc12a1a7f5

Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、`SG`と`ISR`を学んできました

https://qiita.com/ryosuketter/items/442db08856093e6cfeea

## このシリーズの目次

- [このシリーズの方針](https://qiita.com/ryosuketter/items/1142c47913440369b660)
- customHooks を利用してロジックとUIを切り離すまで
    - [ロジックの作成](https://qiita.com/ryosuketter/items/67b168491fd5a186796e)
    - [コンポーネント（UI）作成](https://qiita.com/ryosuketter/items/1dee37be7e4fe62ef6f0)
    - [不要なレンダリングが走らないための工夫（準備編）](https://qiita.com/ryosuketter/items/548dbd70ae835fd66dfb)
    - [不要なレンダリングが走らないための工夫（検証 & 改善編）](https://qiita.com/ryosuketter/items/4fb9b560b0d076491d91) 👈 今回はコレ

## 最終的なゴール

以下のような構成のアプリを作ることです。

![スクリーンショット 2021-10-18 17.27.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/e8a9a2e1-2723-4fec-77ef-be3b9eefdd81.png)

### 目的（最終的なゴールを達成する）

- 仕事で使っている技術のキャッチアップと復習
- 使う可能性がある技術の理解度向上

## 検証用のための記述を追加

<details><summary>長いので折りたたみます（`components/CreateUser.tsx`）</summary><div>

```jsx:components/CreateUser.tsx
import { VFC } from 'react'
import { useCreateForm } from '../hooks/useCreateForm'
import { Child } from './Child'

export const CreateUser: VFC = () => {
  const {
    handleSubmit,
    username,
    usernameChange,
    printMsg,
    text,
    handleTextChange,
  } = useCreateForm()
  return (
    <>
      {console.log('CreateUser rendered')}
      <div className="mb-3 flex flex-col justify-center items-center">
        <label>Text</label>
        <input
          className="px-3 py-2 border border-gray-300"
          type="text"
          value={text}
          onChange={handleTextChange}
        />
      </div>
      <form
        className="flex flex-col justify-center items-center "
        onSubmit={handleSubmit}
      >
        <label>Username</label>
        <input
          className="mb-3 px-3 py-2 border border-gray-300"
          placeholder="New user ?"
          type="text"
          value={username}
          onChange={usernameChange}
        />
        <button
          className="my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none"
          type="submit"
        >
          Submit
        </button>
      </form>
      <Child printMsg={printMsg} handleSubmit={handleSubmit} />
    </>
  )
}
```

</div></details>

<details><summary>長いので折りたたみます（`components/Child.tsx`）</summary><div>

```jsx:components/Child.tsx
import { ChangeEvent, FormEvent, VFC } from 'react'

interface Props {
  printMsg: () => void
}

export const Child: VFC<Props> = ({ printMsg }) => {
  return (
    <>
      {console.log('Child rendered')}
      <p>Child Component</p>
      <button
        className="my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none"
        onClick={printMsg}
      >
        click
      </button>
    </>
  )
}

```

</div></details>

<details><summary>長いので折りたたみます（`hooks/useCreateForm.ts`）</summary><div>

```jsx:hooks/useCreateForm.ts
import { useState, ChangeEvent, FormEvent } from 'react'
import { useMutation } from '@apollo/client'
import { CREATE_USER } from '../queries/queries'
import { CreateUserMutation } from '../types/generated/graphql'

export const useCreateForm = () => {
  const [text, setText] = useState('')
  const [username, setUsername] = useState('')
  const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, {
    update(cache, { data: { insert_users_one } }) {
      const cacheId = cache.identify(insert_users_one)
      cache.modify({
        fields: {
          users(existingUsers, { toReference }) {
            return [toReference(cacheId), ...existingUsers]
          },
        },
      })
    },
  })
  const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value)
  }
  const usernameChange = (e: ChangeEvent<HTMLInputElement>) => {
    setUsername(e.target.value)
  }
  const printMsg = () => {
    console.log('Hello')
  }
  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    try {
      await insert_users_one({
        variables: {
          name: username,
        },
      })
    } catch (err) {
      alert(err.message)
    }
    setUsername('')
  }
  return {
    text,
    handleSubmit,
    username,
    usernameChange,
    printMsg,
    handleTextChange,
  }
}

```

</div></details>

## 実際に検証

以下のタイミングで、`console.log`が発火していることが確認できます。

- components/CreateUser.tsx
    - レンダリング時
        - `console.log('CreateUser rendered')`が発火
- components/Child.tsx
    - レンダリング時
        - `console.log('Child rendered')`が発火
- hooks/useCreateForm.ts
    - 「click」ボタンのクリック時
        - `console.log('Hello')`が発火

![4b29fb37c2b9ddff02fb5e1fdc9d947b.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/cf5d2823-2707-e419-ee62-e317f28b43b1.gif)

## 課題

パフォーマンスの問題があります。

例えば「Text」の直下にある`input`に何か入力しただけでも

![Hooks_memo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/5cf088a2-d98e-aca5-cf2e-63348219cc1a.png)

console上に`Child rendered`が表示されレンダリングされていると分かります。

なぜかというと、親のコンポーネント（`components/CreateUser.tsx`）の`state`が変更されれば、その中になるコンポーネントは全て再度レンダリングされる仕様だからです。

## 課題の解決策の概要

- `components/Child.tsx`のReact`memo`化
- `hooks/useCreateForm.ts`
    - の全ての関数を`useCallback` 化

### components/Child.tsx

```jsx:components/Child.tsx
import { ChangeEvent, FormEvent, memo, VFC } from 'react'

interface Props {
  printMsg: () => void
}

export const Child: VFC<Props> = memo(({ printMsg }) => {
  return (
    <>
      {console.log('Child rendered')}
      <p>Child Component</p>
      <button
        className="my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none"
        onClick={printMsg}
      >
        click
      </button>
    </>
  )
})
```

### hooks/useCreateForm.ts

```jsx:hooks/useCreateForm.ts
import { useState, useCallback, ChangeEvent, FormEvent } from 'react'
import { useMutation } from '@apollo/client'
import { CREATE_USER } from '../queries/queries'
import { CreateUserMutation } from '../types/generated/graphql'

export const useCreateForm = () => {
  const [text, setText] = useState('')
  const [username, setUsername] = useState('')
  const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, {
    update(cache, { data: { insert_users_one } }) {
      const cacheId = cache.identify(insert_users_one)
      cache.modify({
        fields: {
          users(existingUsers, { toReference }) {
            return [toReference(cacheId), ...existingUsers]
          },
        },
      })
    },
  })
  const handleTextChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value)
  }, [])
  const usernameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
    setUsername(e.target.value)
  }, [])
  const printMsg = useCallback(() => {
    console.log('Hello')
  }, [])
  const handleSubmit = useCallback(
    async (e: FormEvent<HTMLFormElement>) => {
      e.preventDefault()
      try {
        await insert_users_one({
          variables: {
            name: username,
          },
        })
      } catch (err) {
        alert(err.message)
      }
      setUsername('')
    },
    [username]
  )
  return {
    text,
    handleSubmit,
    username,
    usernameChange,
    printMsg,
    handleTextChange,
  }
}
```

`useCallback`の`deps`の指定も大事ですが、長くなるので、下記の記事読んで、なんで今回の`deps`の指定はこうなんだ？があれば疑問を解消しておいてください。

また、親（`components/CreateUser.tsx`）から子（`components/Child.tsx`）へpropsとして関数`handleSubmit`を渡したら

ユーザーがブラウザ上で`username`を変えるたびに関数`handleSubmit`は再度レンダリングされます

`<Child printMsg={printMsg} handleSubmit={handleSubmit} />`

それはつまり、`components/Child.tsx`が、`username`を変えるたびに再度レンダリングされるということになります。

ここらへんは、ちゃんと理解して最適化をしていきましょう！

https://qiita.com/seya/items/8291f53576097fc1c52a

## 検証

![7b439983616ed08a3dc03b23adfbb165 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/e6bd8662-9974-2671-e616-437baef6a91a.gif)

親要素のinputの結果、親にあるstateが変化しても、`Child rendered`の発火、つまり子コンポーネントのレンダリングはされなくなりました。

## まとめ

Reactの`memo`や`useCallback`を使った最適化ができました。

全体を通すと、React customHooks を利用してロジックとUIを切り離すまで一連をこれで学べ方かと思います。

## アウトプット100本ノック実施中

https://qiita.com/ryosuketter/items/713474dbd009870e4103