384
243

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 3 years have passed since last update.

React hooksを基礎から理解する (useCallback編+ React.memo)

Last updated at Posted at 2020-06-29

#React hooksとは

React 16.8 で追加された新機能です。
クラスを書かなくても、 stateなどのReactの機能を、関数コンポーネントでシンプルに扱えるようになりました。

↓React.memo, useCallBack, useMemoに関する記事なので、よろしければ参考にしてみてください↓
【React】もっと速くなる!?パフォーマンス最適化に挑戦!

useCallbackとは

useCallbackはパフォーマンス向上のためのフックで、メモ化したコールバック関数を返します。

useEffectと同じように、依存配列(=[deps] コールバック関数が依存している要素が格納された配列)の要素のいずれかが変化した場合のみ、メモ化した値を再計算します。

メモ化とは

メモ化とは同じ結果を返す処理について、初回のみ処理を実行記録しておき、値が必要となった2回目以降は、前回の処理結果を計算することなく呼び出し値を得られるようにすることです。

イベントハンドラーのようなcallback関数をメモ化し、不要に生成される関数インスタンスの作成を抑制、再描画を減らすことにより、都度計算しなくて良くなることからパフォーマンスを向上が期待できます。

基本形

useCallback(callbackFunction, [deps]);

sampleFuncは、再レンダーされる度に新しく作られますが、a,bが変わらない限り、作り直す必要はありません。

const sampleFunc = () => {doSomething(a, b)}

usecallbackを使えば、依存配列の要素a,bのいずれかが変化した場合のみ、以前作ってメモ化したsampleFuncの値を再計算します。一方で全て前回と同じであれば、前回のsampleFuncを再利用します。

const sampleFunc = useCallback(
  () => {doSomething(a, b)}, [a, b]
);

再レンダーによるコストについて検証してみる

Text,Count,Buttonコンポーネントを子に持つ親コンポーネントCounterコンポーネントを作成しました。

testVol1.jsx
import React, {useState} from 'react'

//Titleコンポーネント(子)
const Title = () => {
  console.log('Title component')
  return (
    <h2>useCallBackTest vol.1</h2>
  )
}

//Buttonコンポーネント(子)
const Button = ({handleClick,value}) => {
  console.log('Button child component', value)
  return <button type="button" onClick={handleClick}>{value}</button>
}

//Countコンポーネント(子)
const Count = ({text, countState}) => {
  console.log('Count child component', text)
  return <p>{text}:{countState}</p>
}

//Counterコンポーネント(親)
const Counter = () => {

  const [firstCountState, setFirstCountState] = useState(0)
  const [secondCountState, setSecondCountState] = useState(10)

//+ 1 ボタンのstateセット用関数
  const incrementFirstCounter = () => setFirstCountState(firstCountState + 1)

//+ 10 ボタンのstateセット用関数
  const incrementSecondCounter = () => setSecondCountState(secondCountState + 10)

//子コンポーネントを呼び出す
  return (
    <>
      <Title/>
      <Count text="+ 1 ボタン" countState={firstCountState}/>
      <Count text="+ 10 ボタン" countState={secondCountState}/>
      <Button handleClick={incrementFirstCounter} value={'+1 ボタン'}/>
      <Button handleClick={incrementSecondCounter} value={'+10 ボタン'}/>
    </>
  )
}

export default Counter

console.logを実行しているだけですが、すべてのコンポーネントが再レンダーされています。この部分で高コストな処理を行っていれば、その分だけパフォーマンスに悪影響を与えることになりますし、サイトが大きくなると、負荷も大きくなっていきます。

React.memoについて

React.memoでは、コンポーネントが返した React 要素を記録し、再レンダーされそうになった時に本当に再レンダーが必要かどうかをチェックして、必要な場合のみ再レンダーします。
デフォルトでは、等価性の判断にshallow compareを使っており、オブジェクトの1階層のみを比較することになります。

React.memoは、メモ化したいコンポーネントをラップして使います。

//Titleコンポーネント(子)
const Title = React.memo(() => {
  console.log('Title component')
  return (
    <h2>useCallBackTest vol.1</h2>
  )
})

//Buttonコンポーネント(子)
const Button = React.memo(({handleClick,value}) => {
  console.log('Button child component', value)
  return <button type="button" onClick={handleClick}>{value}</button>
})

//Countコンポーネント(子)
const Count = React.memo(({text, countState}) => {
  console.log('Count child component', text)
  return <p>{text}:{countState}</p>
})

コンポーネントをReact.memoでラップしてメモ化すると、初回にTitleコンポーネント、Countコンポーネント2つ、Buttonコンポーネント2つがすべてレンダリングされました。

2回目以降、Titleコンポーネントについてはpropsがないので再レンダリングされていません。

Countコンポーネントについては、数字が更新されたコンポーネントについてのみ再レンダーされているので、最適化されています。

Buttonコンポーネントについては、ボタンのどちらかをクリックしたときにクリックされていないボタンも合わせ、2つのボタンが再レンダーされているので、最適化出来ていないようです。

Buttonコンポーネントは何故再レンダーされたか

Counter.jsx
//Counterコンポーネント(親)
const Counter = () => {

  const [firstCountState, setFirstCountState] = useState(0)
  const [secondCountState, setSecondCountState] = useState(10)

//+ 1 ボタンのstateセット用関数
  const incrementFirstCounter = () => setFirstCountState(firstCountState + 1)

//+ 10 ボタンのstateセット用関数
  const incrementSecondCounter = () => setSecondCountState(secondCountState + 10)

//子コンポーネントを呼び出す
  return (
    <>
      <Title/>
      <Count text="+ 1 ボタン" countState={firstCountState}/>
      <Count text="+ 10 ボタン" countState={secondCountState}/>
      <Button handleClick={incrementFirstCounter} value={'+1 ボタン'}/>
      <Button handleClick={incrementSecondCounter} value={'+10 ボタン'}/>
    </>
  )
}

<Button handleClick={incrementFirstCounter} value={'+1 ボタン'}/><Button handleClick={incrementSecondCounter} value={'+10 ボタン'}/>の2つのButtonコンポーネントについて、いずれかのボタンをクリックしたときに、stateが更新されるので再レンダーされますが、更新されていないほうのstateのボタンも再レンダーされています。一方のボタンがクリックされて親コンポーネントであるCounterコンポーネントが再レンダーされたタイミングで関数も再生成されており、再生成された関数をReact.memoが別の関数と認識したことによります。

React.memoについてもう少し詳しく

React.memoの第二引数には関数を渡すことができます。
第一引数として前回のprops(prevProps)を、第二引数として今回のprops(nextProps)を受け取ることが出来、真偽値を返すように書くことが出来ます。(areEqual)

const メモ化されたコンポーネント = React.memo(元のコンポーネント, (prevProps, nextProps) => {/* true or flase */})

このareEqual関数はpropsが等しいときにtrueを返し、propsが等しくないときにfalseを返します。
trueを返したときは再レンダーをスキップ、falseを返したときは再レンダーを行います。
(areEqualを省略した場合は、propsのshallow compareで等価性を判断することになります。)
また等価性のチェックにも、当然コストがかかることを考慮しなければなりません。

[React公式サイト(React.memo)] (https://ja.reactjs.org/docs/react-api.html#reactmemo)

useCallbackとReact.memoを組み合わせて最適化

親コンポーネントであるCounterコンポーネントが再レンダーされたタイミングで関数が再生成されないようにするため、useCallbackを使って最適化していきます。

//ReactからuseCallbackをimport
import React, {useState, useCallback} from 'react'

//Titleコンポーネント(子)
//React.memoでラップ
const Title = React.memo(() => {
  console.log('Title component')
  return (
    <h2>useCallBackTest vol.1</h2>
  )
})

//Buttonコンポーネント(子)
//React.memoでラップ
const Button = React.memo(({handleClick,value}) => {
  console.log('Button child component', value)
  return <button type="button" onClick={handleClick}>{value}</button>
})

//Countコンポーネント(子)
//React.memoでラップ
const Count = React.memo(({text, countState}) => {
  console.log('Count child component', text)
  return <p>{text}:{countState}</p>
})

//Counterコンポーネント(親)
const Counter = () => {

  const [firstCountState, setFirstCountState] = useState(0)
  const [secondCountState, setSecondCountState] = useState(10)

//+ 1 ボタンのstateセット用関数
//useCallbackで関数をラップし、依存配列には関数内で利用しているfirstCountStateを入れます。
  const incrementFirstCounter = useCallback(() => setFirstCountState(firstCountState + 1),[firstCountState])

//+ 10 ボタンのstateセット用関数
//useCallbackで関数をラップし、依存配列には関数内で利用しているsecondCountStateを入れます。
  const incrementSecondCounter = useCallback(() => setSecondCountState(secondCountState + 10),[secondCountState])

//子コンポーネントを呼び出す
  return (
    <>
      <Title/>
      <Count text="+ 1 ボタン" countState={firstCountState}/>
      <Count text="+ 10 ボタン" countState={secondCountState}/>
      <Button handleClick={incrementFirstCounter} value={'+1 ボタン'}/>
      <Button handleClick={incrementSecondCounter} value={'+10 ボタン'}/>
    </>
  )
}

export default Counter

うまく最適化出来ました!
useCallbackでメモ化されたコールバック関数は、React.memoでメモ化されたコンポーネントへ渡して利用することで初めて不要な再描画をスキップ出来るようになります。
React.memoとuseCallback、さっそくリファクタリングに役立ちそうです:smiley:

React.memo/useCallback/useMemo関連の記事を書き直しましたので、よろしければどうぞ!!

384
243
1

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
384
243

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?