今までの‘useCallback’の使用姿勢は間違っています.


gitHubノートから整理する.
一、落とし穴:useCallbackは、関数構成要素の多すぎる内部関数による性能問題を解決するためのものである.
関数コンポーネントを使用する時、しばしばいくつかの内部関数を定義しますが、これは関数コンポーネントの性能に影響すると思います.useCallbackはこの問題を解決するものだと思っていますが、そうではないです.
  • Aree Hook s slow because of creating functions in render?;

  • classより軽い関数コンポーネントとHOC、renderPropsなどの追加の階層を回避したおかげで、関数コンポーネントの性能はそこまで差がないです.
  • 実はuseCallbackを使用すると、追加の性能をもたらします.追加のdeps変更判断が追加されたためです.
  • useCallbackも、内部関数の再作成の問題を解決するわけではない.よく見ると、useCallbackを使用するかどうかにかかわらず、内部関数の再作成は避けられない.
    export default function Index() {
        const [clickCount, increaseCount] = useState(0);
        //     `useCallback`,              
        const handleClick = () => {
            console.log('handleClick');
            increaseCount(clickCount + 1);
        }
    
        //   `useCallback`,                  `useCallback`   
        const handleClick = useCallback(() => {
            console.log('handleClick');
            increaseCount(clickCount + 1);
        }, [])
    
        return (
            

    {clickCount}

    ) }
  • 二、useCallback解決された問題useCallbackは、実はmemoizeを利用して、不必要なサブアセンブリの再レンダリングを低減するものである.
    import React, { useState, useCallback } from 'react'
    
    function Button(props) {
        const { handleClick, children } = props;
        console.log('Button -> render');
    
        return (
            
        )
    }
    
    const MemoizedButton = React.memo(Button);
    
    export default function Index() {
        const [clickCount, increaseCount] = useState(0);
        
        const handleClick = () => {
            console.log('handleClick');
            increaseCount(clickCount + 1);
        }
    
        return (
            

    {clickCount}

    Click
    ) }
    React.memoを使用してButtonコンポーネントを修正しても、【Click】btnをクリックするたびに、Buttonコンポーネントがレンダリングされることになる.
  • Indexコンポーネントstateが変化し、コンポーネントが再レンダリングされる.
  • レンダリングのたびに内部関数handleClick を再作成し、
  • は、さらに、サブアセンブリButtonもレンダリングされる.
  • useCallbackを使用して最適化する:
    import React, { useState, useCallback } from 'react'
    
    function Button(props) {
        const { handleClick, children } = props;
        console.log('Button -> render');
    
        return (
            
        )
    }
    
    const MemoizedButton = React.memo(Button);
    
    export default function Index() {
        const [clickCount, increaseCount] = useState(0);
        //      `useCallback`
        const handleClick = useCallback(() => {
            console.log('handleClick');
            increaseCount(clickCount + 1);
        }, [])
    
        return (
            

    {clickCount}

    Click
    ) }
    三、useCallbackの問題
    3.1 useCallbackの実パラメータ関数によって読み取られる変数は変化する(一般的にstate、propsから).
    export default function Index() {
        const [text, updateText] = useState('Initial value');
    
        const handleSubmit = useCallback(() => {
            console.log(`Text: ${text}`); // BUG:         
        }, []);
    
        return (
            <>
                 updateText(e.target.value)} />
                

    useCallback(fn, deps)

    > ) }
    input値を変更し、handleSubmit の処理関数は依然として初期値を出力する.useCallbackの実パラメータ関数で読み取られた変数が変化している場合は、依存配列に記入してください.
    export default function Index() {
        const [text, updateText] = useState('Initial value');
    
        const handleSubmit = useCallback(() => {
            console.log(`Text: ${text}`); //          
        }, [text]); //  `text`       
    
        return (
            <>
                 updateText(e.target.value)} />
                

    useCallback(fn, deps)

    > ) }
    問題が解決されましたが、プログラムは一番良くないです.input入力ボックスの変化が頻繁すぎるので、useCallbackの存在意義は必要ないです.
    3.2 JS内部関数の作成は非常に速いです。これは性能の問題ではありません。
    上の例です.もしサブアセンブリが時間がかかると、問題が露呈します.
    //   :ExpensiveTree         `React.memo`   ,           
    const ExpensiveTree = React.memo(function (props) {
        console.log('Render ExpensiveTree')
        const { onClick } = props;
        const dateBegin = Date.now();
        //      ,        ,     
        while(Date.now() - dateBegin < 600) {}
    
        useEffect(() => {
            console.log('Render ExpensiveTree --- DONE')
        })
    
        return (
            

    ) }); export default function Index() { const [text, updateText] = useState('Initial value'); const handleSubmit = useCallback(() => { console.log(`Text: ${text}`); }, [text]); return ( <> updateText(e.target.value)} /> > ) }
    問題:input値を更新して、カートンを比較することを発見しました.
    3.2.1 How to read an offten-change value from useCallback?
    最適化の考え方:
  • サブアセンブリuseRefの無効な再レンダリングを避けるためには、親コンポーネントre−renderのときのExpensiveTreeの属性値が不変であることを保証しなければならない.
  • は、handleSubmit属性値が不変である場合にも、最新のstateにアクセスできるようにする.
  • export default function Index() {
        const [text, updateText] = useState('Initial value');
        const textRef = useRef(text);
    
        const handleSubmit = useCallback(() => {
            console.log(`Text: ${textRef.current}`);
        }, [textRef]);
    
        useEffect(() => {
            console.log('update text')
            textRef.current = text;
        }, [text])
    
        return (
            <>
                 updateText(e.target.value)} />
                
            >
        )
    }
    原理:
  • handleSubmitは、以前の直接依存handleSubmitによってtextになりました.re-renderの度にtextRefは不変であるため、textRefは不変です.
  • は、handleSubmitの更新毎にtextを更新する.このようにtextRef.currentは変わらないが、handleSubmitを介して最新の値にアクセスすることもできる.
  • textRefuseRefこのような解決方法は、固定された「モード」を形成することができる.
    export default function Index() {
        const [text, updateText] = useState('Initial value');
    
        const handleSubmit = useEffectCallback(() => {
            console.log(`Text: ${text}`);
        }, [text]);
    
        return (
            <>
                 updateText(e.target.value)} />
                
            >
        )
    }
    
    function useEffectCallback(fn, dependencies) {
        const ref = useRef(null);
    
        useEffect(() => {
            ref.current = fn;
        }, [fn, ...dependencies])
    
        return useCallback(() => {
            ref.current && ref.current(); //   ref.current         
        }, [ref])
    }
  • は、useEffectを介して変化の値を保持し、
  • は、useRefを介して変化の値を更新する.
  • は、useEffectを介して固定されたコールバックを返す.
  • 3.2.2 useCallbackソリューション
    const ExpensiveTreeDispatch = React.memo(function (props) {
        console.log('Render ExpensiveTree')
        const { dispatch } = props;
        const dateBegin = Date.now();
        //      ,        ,     
        while(Date.now() - dateBegin < 600) {}
    
        useEffect(() => {
            console.log('Render ExpensiveTree --- DONE')
        })
    
        return (
            
    { dispatch({type: 'log' })}}>

    ) }); function reducer(state, action) { switch(action.type) { case 'update': return action.preload; case 'log': console.log(`Text: ${state}`); return state; } } export default function Index() { const [text, dispatch] = useReducer(reducer, 'Initial value'); return ( <> dispatch({ type: 'update', preload: e.target.value })} /> > ) }
    原理:
  • useReducerdispatchを持っています.re-renderの時には変化がありません.
  • は、memoize関数で最新のreducerを取得することができる.
  • stateソリューション
    React公式推奨はcontextを介してcalback方式を伝える代わりにpropsを使用する.上記の例をcontextリレーcallback関数に変更しました.
    function reducer(state, action) {
        switch(action.type) {
            case 'update':
                return action.preload;
            case 'log':
                console.log(`Text: ${state}`);   
                return state;     
        }
    }
    
    const TextUpdateDispatch = React.createContext(null);
    
    export default function Index() {
        const [text, dispatch] = useReducer(reducer, 'Initial value');
    
        return (
            
                 dispatch({
                    type: 'update', 
                    preload: e.target.value
                })} />
                
            
        )
    }
    
    const ExpensiveTreeDispatchContext = React.memo(function (props) {
        console.log('Render ExpensiveTree')
        //  `context`  `dispatch`
        const dispatch = useContext(TextUpdateDispatch);
    
        const dateBegin = Date.now();
        //      ,        ,     
        while(Date.now() - dateBegin < 600) {}
    
        useEffect(() => {
            console.log('Render ExpensiveTree --- DONE')
        })
    
        return (
            
    { dispatch({type: 'log' })}}>

    ) });