useMemo、useCallbackを本気で理解する
ゴール
useMemo、useCallbackを本気で理解し、人に説明できる
useCallback
React.memoでメモ化されたコンポーネントのpropsの中にコールバック関数がある場合、
親の依存配列で指定したstate以外のstateを更新しても再レンダリングされるので、コールバック関数渡しても再レンダリングしない様にするときに使う?
をuseCallbackを使い再レンダリングを制御する
// ChildやChildPropsは下記リンクと一緒
// https://zenn.dev/nus3/scraps/8de787b8a04291#comment-e1893477aead39
const ChildMemo = memo<ChildProps>(({ count1, handleClick }) => {
return <Child count1={count1} handleClick={handleClick} />
})
const Parent: NextPage = () => {
const [parentCount, setParentCount] = useState<number>(0)
const [childCount1, setChildCount1] = useState<number>(0)
useEffect(() => {
// eslint-disable-next-line
console.log('Parentがレンダリングされたよ')
})
// 命名ダサいのは許して
const handleClickCallback = useCallback(() => {
// eslint-disable-next-line
console.log('click button')
}, [])
return (
<div style={{ padding: '50px' }}>
<button
type="button"
onClick={() => {
setParentCount(parentCount + 1)
}}
>
Parent count up
</button>
<button
type="button"
onClick={() => {
setChildCount1(childCount1 + 1)
}}
>
Child1 count up
</button>
<p>Parent:{parentCount}</p>
<ChildMemo count1={childCount1} handleClick={handleClickCallback} />
</div>
)
}
callbackに引数があるとき
- const handleClickCallback = useCallback(() => {
- // eslint-disable-next-line
- console.log('click button')
- }, [])
+ const handleClick = (echo: string) => {
+ // eslint-disable-next-line
+ console.log(echo)
+ }
+ const handleClickCallback = useCallback((echo: string) => {
+ handleClick(echo)
+ }, [])
callback内で親のstateを使うとき
依存配列に使うstateを入れる
- const handleClickCallback = useCallback(() => {
- // eslint-disable-next-line
- console.log('click button')
- }, [])
+ const handleClickCallback = useCallback(() => {
+ // eslint-disable-next-line
+ console.log(childCount1)
+ }, [childCount1])
useCallback(fn, deps) は useMemo(() => fn, deps) と等価です。
https://ja.reactjs.org/docs/hooks-reference.html#usecallback
useMemo
基本的にコンポーネントで使われているstateが更新されてると再レンダリングされる
以下の例ではparentCountが更新されるたびに再レンダリングされる
const Parent: NextPage = () => {
const [parentCount, setParentCount] = useState<number>(0)
useEffect(() => {
// eslint-disable-next-line
console.log('Parentがレンダリングされたよ')
})
return (
<div style={{ padding: '50px' }}>
<button
type="button"
onClick={() => {
setParentCount(parentCount + 1)
}}
>
Parent Count up
</button>
<p>Parent:{parentCount}</p>
</div>
)
}
子コンポーネントが親コンポーネントのstateに依存していなくても再レンダリングされる
以下では子コンポーネントは親のstateをpropで受け取ってないが親でsetParentCountをされる(親のstateが更新)たびに子コンポーネントのレンダリングもされる
const NothingPropsChild = (): JSX.Element => {
useEffect(() => {
// eslint-disable-next-line
console.log('NothingPropsChildがレンダリングされたよ')
})
return <p>propがないコンポーネントだよ</p>
}
const Parent: NextPage = () => {
const [parentCount, setParentCount] = useState<number>(0)
useEffect(() => {
// eslint-disable-next-line
console.log('Parentがレンダリングされたよ')
})
return (
<div style={{ padding: '50px' }}>
<button
type="button"
onClick={() => {
setParentCount(parentCount + 1)
}}
>
Parent count up
</button>
<p>Parent:{parentCount}</p>
<NothingPropsChild />
</div>
)
}
子コンポーネントのstateを更新した場合は子コンポーネントのみが再レンダリングされる
NothingPropsChild count up
ボタンを押すとNothingPropsChildがレンダリングされたよ
だけがconsoleに出力される
const NothingPropsChild = (): JSX.Element => {
const [count, setCount] = useState<number>(0)
useEffect(() => {
// eslint-disable-next-line
console.log('NothingPropsChildがレンダリングされたよ')
})
return (
<div>
<p>propがないコンポーネントだよ</p>
<button
type="button"
onClick={() => {
setCount(count + 1)
}}
>
NothingPropsChild count up
</button>
<p>NothingPropsChild:{count}</p>
</div>
)
}
const Parent: NextPage = () => {
const [parentCount, setParentCount] = useState<number>(0)
useEffect(() => {
// eslint-disable-next-line
console.log('Parentがレンダリングされたよ')
})
return (
<div style={{ padding: '50px' }}>
<button
type="button"
onClick={() => {
setParentCount(parentCount + 1)
}}
>
Parent count up
</button>
<p>Parent:{parentCount}</p>
<NothingPropsChild />
</div>
)
}
useMemo使ってみる
以下の場合、Parent count up
で親のstateを変更してもChildは再レンダリングされない
Child1 count up
すると両方が再レンダリングされる
useMemoの第二引数が変更されない限りChildは再レンダリングされない
type ChildProps = {
count1: number
}
const Child = ({ count1 }: ChildProps): JSX.Element => {
useEffect(() => {
// eslint-disable-next-line
console.log('Childがレンダリングされたよ')
})
return <p>Child1:{count1}</p>
}
const Parent: NextPage = () => {
const [parentCount, setParentCount] = useState<number>(0)
const [childCount1, setChildCount1] = useState<number>(0)
useEffect(() => {
// eslint-disable-next-line
console.log('Parentがレンダリングされたよ')
})
const memoChild = useMemo(() => {
return <Child count1={childCount1} />
}, [childCount1])
return (
<div style={{ padding: '50px' }}>
<button
type="button"
onClick={() => {
setParentCount(parentCount + 1)
}}
>
Parent count up
</button>
<button
type="button"
onClick={() => {
setChildCount1(childCount1 + 1)
}}
>
Child1 count up
</button>
<p>Parent:{parentCount}</p>
{memoChild}
</div>
)
}
useMemoの第二引数に入れるべき値を入れなかった場合どうなるか
setChildCount2
してもChildコンポーネントは再レンダリングされない
type ChildProps = {
count1: number
count2: number
}
const Child = ({ count1, count2 }: ChildProps): JSX.Element => {
useEffect(() => {
// eslint-disable-next-line
console.log('Childがレンダリングされたよ')
})
return (
<>
<p>Child1:{count1}</p>
<p>Child2:{count2}</p>
</>
)
}
const Parent: NextPage = () => {
const [parentCount, setParentCount] = useState<number>(0)
const [childCount1, setChildCount1] = useState<number>(0)
const [childCount2, setChildCount2] = useState<number>(0)
useEffect(() => {
// eslint-disable-next-line
console.log('Parentがレンダリングされたよ')
})
// NOTE: useMemoの第二引数にはchildCount2が基本的には入ってるべきでeslint-plugin-react-hooks パッケージの exhaustive-deps ルール入れると多分はじかれる書き方
const memoChild = useMemo(() => {
return <Child count1={childCount1} count2={childCount2} />
}, [childCount1])
return (
<div style={{ padding: '50px' }}>
<button
type="button"
onClick={() => {
setParentCount(parentCount + 1)
}}
>
Parent count up
</button>
<button
type="button"
onClick={() => {
setChildCount1(childCount1 + 1)
}}
>
Child1 count up
</button>
<button
type="button"
onClick={() => {
setChildCount2(childCount2 + 1)
}}
>
Child2 count up
</button>
<p>Parent:{parentCount}</p>
{memoChild}
</div>
)
}
useMemoでpropsにcallback関数を渡した場合
子のコンポーネントは再レンダリングされない(useMemoの第二引数にcallback関数入れてないから当たり前っちゃ当たり前?)
type ChildProps = {
count1: number
handleClick: () => void
}
const Child = ({ count1, handleClick }: ChildProps): JSX.Element => {
useEffect(() => {
// eslint-disable-next-line
console.log('Childがレンダリングされたよ')
})
return (
<>
<p>Child1:{count1}</p>
<button
type="button"
onClick={() => {
handleClick()
}}
>
ボタン
</button>
</>
)
}
const Parent: NextPage = () => {
const [parentCount, setParentCount] = useState<number>(0)
const [childCount1, setChildCount1] = useState<number>(0)
useEffect(() => {
// eslint-disable-next-line
console.log('Parentがレンダリングされたよ')
})
const handleClick = () => {
// eslint-disable-next-line
console.log('click button')
}
// NOTE: useMemoの第二引数にはchildCount2が基本的には入ってるべきでeslint-plugin-react-hooks パッケージの exhaustive-deps ルール入れると多分はじかれる書き方
const memoChild = useMemo(() => {
return <Child count1={childCount1} handleClick={handleClick} />
}, [childCount1])
return (
<div style={{ padding: '50px' }}>
<button
type="button"
onClick={() => {
setParentCount(parentCount + 1)
}}
>
Parent count up
</button>
<button
type="button"
onClick={() => {
setChildCount1(childCount1 + 1)
}}
>
Child1 count up
</button>
<p>Parent:{parentCount}</p>
{memoChild}
</div>
)
}
React.memoでpropsにcallback関数を渡した場合
childCount1を更新せずとも再レンダリングされる
// Childは人つ前のスレッドと同じ定義
const ChildMemo = memo<ChildProps>(({ count1 }) => {
return <Child count1={count1} handleClick={() => undefined} />
})
const Parent: NextPage = () => {
const [parentCount, setParentCount] = useState<number>(0)
const [childCount1, setChildCount1] = useState<number>(0)
useEffect(() => {
// eslint-disable-next-line
console.log('Parentがレンダリングされたよ')
})
const handleClick = () => {
// eslint-disable-next-line
console.log('click button')
}
return (
<div style={{ padding: '50px' }}>
<button
type="button"
onClick={() => {
setParentCount(parentCount + 1)
}}
>
Parent count up
</button>
<button
type="button"
onClick={() => {
setChildCount1(childCount1 + 1)
}}
>
Child1 count up
</button>
<p>Parent:{parentCount}</p>
<ChildMemo count1={childCount1} handleClick={handleClick} />
</div>
)
}
useReducerとmemoの組み合わせ
useReducerで定義したオブジェクトの一部のプロパティをpropとしてわけていた場合、dispatchでimmutableな実装でアップデート処理書いてたら再レンダリングはされるのかどうか
useReducer周り
export type State = {
count1: number
count2: number
count3: number
count4: number
}
export const updateCount1 = (
state: State,
{ value }: UpdateCount1Payload
): State => ({ ...state, count1: value })
export const reducer: Reducer<State, Action> = (state, action) => {
switch (action.type) {
case ActionType.UpdateCount1:
return updateCount1(state, action.payload)
// ... UpdateCount2, UpdateCount3, UpdateCount4が続く
default:
throw new TypeError(`unexpected action. ${action}`)
}
子コンポーネント
const Child = ({ count, label }: ChildProps): JSX.Element => {
useEffect(() => {
// eslint-disable-next-line
console.log(`Child${label}がレンダリングされたよ`)
})
return (
<p>
Child{label}:{count}
</p>
)
}
const ChildMemo = memo<ChildProps>(({ count, label }) => {
return <Child count={count} label={label} />
})
親コンポーネント
const Parent: NextPage = () => {
const [parentCount, setParentCount] = useState<number>(0)
const [childState, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
// eslint-disable-next-line
console.log('Parentがレンダリングされたよ')
})
return (
<div style={{ padding: '50px' }}>
<button
type="button"
onClick={() => {
setParentCount(parentCount + 1)
}}
>
Parent count up
</button>
<button
type="button"
onClick={() => {
dispatch({
type: ActionType.UpdateCount1,
payload: {
value: childState.count1 + 1,
},
})
}}
>
Child1 count up
</button>
{/* count2, count3...の値をカウントアップするようのボタンが同様にある */}
<p>Parent:{parentCount}</p>
<ChildMemo count={childState.count1} label="01" />
{/* count2, count3...の値をcountのpropに渡すChildMemoコンポーネントがある */}
</div>
)
}
こんな感じの画面
単純にuseReducerのプロパティをmemo化された子コンポーネントのpropとして渡してみる
- 親のstateを更新する(setParentCount)→memo化された子コンポーネントはレンダリングされない
- dispatchしてみる(stateの一部のオブジェクトではないプロパティを更新)→immutableなはずだが変更されたプロパティをpropsで使ってるコンポーネントしか再レンダリングされない
stateのプロパティの値とコールバック関数をpropsに持つ場合
子コンポーネント
+ <>
<p>
Child{label}:{count}
</p>
+ <button
+ type="button"
+ onClick={() => {
+ handleClick()
+ }}
+ >
+ ボタン
+ </button>
+ </>
子コンポーネントのメモ化
+ const ChildMemo = memo<ChildProps>(({ count, label, handleClick }) => {
+ return <Child count={count} label={label} handleClick={handleClick} />
+ })
親コンポーネント
+ const handleClick = useCallback(() => {
+ // eslint-disable-next-line
+ console.log('click button')
+ }, [])
// いろいろ省略....
<ChildMemo
count={childState.count1}
label="01"
+ handleClick={handleClick}
/>
- callback関数をpropに持したので親のstateが更新されたら子コンポーネントもレンダリングされる
- useCallbackすれば変更されたプロパティをpropsで使ってるコンポーネントしか再レンダリングされない