7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-11-29

はじめに

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

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

(ReactのmemouseCallbackを使った最適化を説明したいがためのコンポーネントの配備)

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

これまで

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

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

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

このシリーズの目次

最終的なゴール

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

スクリーンショット 2021-10-18 17.27.41.png

目的(最終的なゴールを達成する)

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

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

長いので折りたたみます(`components/CreateUser.tsx`)
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} />
    </>
  )
}
長いので折りたたみます(`components/Child.tsx`)
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>
    </>
  )
}

長いので折りたたみます(`hooks/useCreateForm.ts`)
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,
  }
}

実際に検証

以下のタイミングで、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

課題

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

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

Hooks_memo.png

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

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

課題の解決策の概要

  • components/Child.tsxのReactmemo
  • hooks/useCreateForm.ts
    • の全ての関数をuseCallback

components/Child.tsx

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

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,
  }
}

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

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

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

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

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

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

検証

7b439983616ed08a3dc03b23adfbb165 (1).gif

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

まとめ

ReactのmemouseCallbackを使った最適化ができました。

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

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

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?