Closed23

useMemo、useCallbackを本気で理解する

nus3nus3

useCallback

https://ja.reactjs.org/docs/hooks-reference.html#usecallback

React.memoでメモ化されたコンポーネントのpropsの中にコールバック関数がある場合、
親の依存配列で指定したstate以外のstateを更新しても再レンダリングされるので、コールバック関数渡しても再レンダリングしない様にするときに使う?

nus3nus3

https://zenn.dev/nus3/scraps/8de787b8a04291#comment-e1893477aead39
を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>
  )
}
nus3nus3

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)
+  }, [])
nus3nus3

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])
nus3nus3
nus3nus3

基本的にコンポーネントで使われている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>
  )
}
nus3nus3

子コンポーネントが親コンポーネントの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>
  )
}
nus3nus3

子コンポーネントの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>
  )
}
nus3nus3

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>
  )
}
nus3nus3

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>
  )
}
nus3nus3

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>
  )
}
nus3nus3

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>
  )
}
nus3nus3

useReducerとmemoの組み合わせ

useReducerで定義したオブジェクトの一部のプロパティをpropとしてわけていた場合、dispatchでimmutableな実装でアップデート処理書いてたら再レンダリングはされるのかどうか

nus3nus3

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>
  )
}
nus3nus3

単純にuseReducerのプロパティをmemo化された子コンポーネントのpropとして渡してみる

  • 親のstateを更新する(setParentCount)→memo化された子コンポーネントはレンダリングされない
  • dispatchしてみる(stateの一部のオブジェクトではないプロパティを更新)→immutableなはずだが変更されたプロパティをpropsで使ってるコンポーネントしか再レンダリングされない
nus3nus3

stateのプロパティの値とコールバック関数をpropsに持つ場合

nus3nus3

子コンポーネント

+    <>
        <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}
      />
nus3nus3
  • callback関数をpropに持したので親のstateが更新されたら子コンポーネントもレンダリングされる
  • useCallbackすれば変更されたプロパティをpropsで使ってるコンポーネントしか再レンダリングされない
このスクラップは2021/02/26にクローズされました