はじめに
React customHooks を利用してロジックとUIを切り離すまでをやってみたシリーズ
前回は、不要なレンダリングが走らないための工夫を紹介するための準備をしました。
(Reactのmemo
やuseCallback
を使った最適化を説明したいがためのコンポーネントの配備)
今回は、その実践編をやっていきます。
これまで
Hasuraにユーザーのデータと仮定してデータを作成し
データを外部APIとして取得できるようにして、Next.js(apollo)を使って、クエリやミューテーションできるようにしました
Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、SG
とISR
を学んできました
このシリーズの目次
- このシリーズの方針
- customHooks を利用してロジックとUIを切り離すまで
最終的なゴール
以下のような構成のアプリを作ることです。
目的(最終的なゴールを達成する)
- 仕事で使っている技術のキャッチアップと復習
- 使う可能性がある技術の理解度向上
検証用のための記述を追加
長いので折りたたみます(`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`)
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`)
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')
が発火
-
- 「click」ボタンのクリック時
課題
パフォーマンスの問題があります。
例えば「Text」の直下にあるinput
に何か入力しただけでも
console上にChild rendered
が表示されレンダリングされていると分かります。
なぜかというと、親のコンポーネント(components/CreateUser.tsx
)のstate
が変更されれば、その中になるコンポーネントは全て再度レンダリングされる仕様だからです。
課題の解決策の概要
-
components/Child.tsx
のReactmemo
化 -
hooks/useCreateForm.ts
- の全ての関数を
useCallback
化
- の全ての関数を
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
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
を変えるたびに再度レンダリングされるということになります。
ここらへんは、ちゃんと理解して最適化をしていきましょう!
検証
親要素のinputの結果、親にあるstateが変化しても、Child rendered
の発火、つまり子コンポーネントのレンダリングはされなくなりました。
まとめ
Reactのmemo
やuseCallback
を使った最適化ができました。
全体を通すと、React customHooks を利用してロジックとUIを切り離すまで一連をこれで学べ方かと思います。
アウトプット100本ノック実施中