React Hooksで関数の再作成を抑制する useEventCallback
React Hooksを利用した関数コンポーネントでは、useCallback
を利用することで関数をメモ化し再利用することができます。
これにより、memo化された子コンポーネントにおいて、propsの不要な更新によるre-renderを避けることができます。
// 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} />
</>
);
};
// 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
というカスタムhooksを作るという方法があります。
https://github.com/facebook/react/issues/14099#issuecomment-440013892
https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback
上記の2つはそれぞれ実装がちょっと異なっていますが、やろうとしていることはだいたい同じです。
TypeScriptで型を意識して書くとこんなかんじでしょうか。
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
)を用いると、先の例は以下のようになります
// カスタム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
でやってればそのへん問題なさそうにも思えるのですが、まずいケースが出てくる(あるいは今後出てこないことを保証できない)ということでしょうか。
Author And Source
この問題について(React Hooksで関数の再作成を抑制する useEventCallback), 我々は、より多くの情報をここで見つけました https://qiita.com/stomita/items/4bb5068c219ca16d9823著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .