React Hooks에서 함수 재작성 억제 useEventCallback

19327 단어 Reactreact-hooks
React Hooks를 사용하는 함수 구성 요소의 경우 useCallback를 사용하여 함수를 기록하고 재사용할 수 있습니다.
이렇게하면 memo 화 된 하위 구성 요소에서 props의 불필요한 업데이트로 인한 re-render를 피할 수 있습니다.

useCallback 사용 전
// Buttonコンポーネントはpropsがshallow equalであればrenderされない
const Button = React.memo(props => {
  const { label, onClick } = props;
  console.log(`Button "${label}" is rendered`);
  return <button onClick={onClick}>{label}</button>;
});

//
export const CounterComp = props => {
  const [count, setCount] = useState(0);
  // onIncrement/onDecrement は render実行ごとに毎回異なるものが生成される
  const onIncrement = () => setCount(count + 1);
  const onDecrement = () => setCount(count - 1);
  // ゆえに下の`Button`は毎回renderされる
  return (
    <>
      <div>Count: {count}</div>
      <Button label="+" onClick={onIncrement} />
      <Button label="-" onClick={onDecrement} />
    </>
  );
};




useCallback 사용 후
// Buttonコンポーネントはpropsがshallow equalであればrenderされない
const Button = React.memo(props => {
  const { label, onClick } = props;
  console.log(`Button "${label}" is rendered`);
  return <button onClick={onClick}>{label}</button>;
});

//
export const CounterComp = props => {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
  // onIncCountA/onIncCountB は再利用される
  const onIncCountA = useCallback(() => setCountA(countA + 1), [countA]);
  const onIncCountB = useCallback(() => setCountB(countB + 1), [countB]);
  // 片方のcounterが更新されても、もう一方の`Button`のrenderは避けられる
  return (
    <>
      <div>
        A = {countA}, B = {countB}
      </div>
      <Button label="A++" onClick={onIncCountA} />
      <Button label="B++" onClick={onIncCountB} />
    </>
  );
};



그러나, useCallback 를 이용하고 있어도 의존하는 변수가 업데이트 되고 있으면 역시 콜백은 새롭게 재작성되어 버려, Button 컴퍼넌트에 있어서 불필요한 render 가 발생해 버립니다.

이를 피하기 위해 useEventCallback라는 사용자 정의 후크를 만드는 방법이 있습니다.

htps : // 기주 b. 코 m / 후세 보오 k / 레아 ct / 이스에 s / 14099 # 이스에 코멘 t-440013892
htps : // Rea ctjs. rg/do cs/호오 ks-후 q. html # HO W-T-REA-D-AN-O-FEN-changin-g

위의 두 가지는 각각 구현이 약간 다릅니다만, 하려고 하는 것은 대체로 같습니다.
TypeScript로 형식을 의식하고 쓰면 이런 느낌입니까?

useEventCallback.ts
import { useRef, useCallback, useLayoutEffect } from 'react';

export function useEventCallback<A extends any[], R>(
  callback: (...args: A) => R,
): (...args: A) => R {
  const callbackRef = useRef<typeof callback>(() => {
    throw new Error('Cannot call an event handler while rendering.');
  });
  useLayoutEffect(() => {
    callbackRef.current = callback;
  }, [callback]);
  return useCallback(
    (...args: A) => {
      const callback = callbackRef.current;
      return callback(...args);
    },
    [],
  );
}

이것 (useEventCallback)을 사용하면 앞의 예는 다음과 같습니다.

useEventCallback 사용
// カスタムHook
function useEventCallback(callback) {
  const callbackRef = useRef();
  useLayoutEffect(() => {
    callbackRef.current = callback;
  }, [callback]);
  return useCallback((...args) => {
    const callback = callbackRef.current;
    return callback(...args);
  }, []);
}

// Buttonコンポーネントはpropsがshallow equalであればrenderされない
const Button = React.memo(props => {
  const { label, onClick } = props;
  console.log(`Button "${label}" is rendered`);
  return <button onClick={onClick}>{label}</button>;
});

//
export const CounterComp = props => {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
  // onIncCountA/onIncCountB は常に同じ関数が再利用される
  const onIncCountA = useEventCallback(() => setCountA(countA + 1));
  const onIncCountB = useEventCallback(() => setCountB(countB + 1));
  // `Button`のre-renderは常に避けられる
  return (
    <>
      <div>
        A = {countA}, B = {countB}
      </div>
      <Button label="A++" onClick={onIncCountA} />
      <Button label="B++" onClick={onIncCountB} />
    </>
  );
};



매우 간단하고 잘 보입니다.

다만, 이 방법은 상기의 Hooks FAQ 에서는 그다지 추천하지 않는다, 라고 하는 느낌이군요.

Note
We recommend to pass dispatch down in context rather than individual callbacks in props. The approach below is only mentioned here for completeness and as an escape hatch.
Also note that this pattern might cause problems in the concurrent mode. We plan to provide more ergonomic alternatives in the future, but the safest solution right now is to always invalidate the callback if some value it depends on changes.

흠, Concurrent Mode 로 문제가 나올지도 모릅니다.
다만, render 페이즈로 callback ref를 mutate 하는 것이 아니라 useLayoutEffect 로 하면 그 별 문제 없을 것 같아도 생각됩니다만, 우선 케이스가 나온다(혹은 향후 나오지 않을 것을 보증할 수 없다) 그렇죠?

좋은 웹페이지 즐겨찾기