乐趣区

关于react.js:react-useCallback-闭包陷阱

1. 为什么须要 useCallback

  • 在应用 useCallback 和 useMemo 钩子之前,如果每次批改 parent 的 count,都会导致 child 从新渲染
const Child = (props) => {console.log('Child render');
  return (
    <div>
      <button onClick={props.clickCallback}>Child click</button>
    </div>
  )
}

const parent = () => {const [count, setCount] = useState(1);
  const cb = () => {console.log(count);
  };
  
  return (
    <div>
      parent
      <button onClick={() => setCount()}>click</button>
      <Child clickCallback={cb} />
    </div>
  )
}

2. 场景复现

  • 函数组件中,个别 useCallback 和 useMemo 都是搭配应用,模仿 componentShouldUpdate 钩子,防止不必要的渲染。优化之后,咱们就发现,在 parent 中,一直的 setCount 批改状态,也不会触发 Child 组件的更新。彷佛解决了 父函数组件更新状态,导致子组件的不必要更新问题。然而当点击子组件的 button 时,就会发现,打印的是 count 的初始值 1,这也就是 useCallback 的闭包陷阱,不能拿到最新的 state。
const Child = (props) => {console.log('Child render');
  return (
    <div>
      <button onClick={props.clickCallback}>Child click</button>
    </div>
  )
}

const parent = () => {const [count, setCount] = useState(1);
  
  const cb = React.useCallback(() => {console.log(count);
  }, []);

  const ChildMemo = React.useMemo(() => <Child clickCallback={cb} />, [cb]);

  return (
    <div style={{ 
      width: '100vw', height: '100vh', 
      justifyContent: 'center', alignItems: 'center', 
      display: 'flex', 
      flexDirection: 'column' }}
     >
      parent count : {count}
      <button onClick={() => setCount(count + 1)}>click</button>
      {ChildMemo}
    </div>
  )
}

3. 解决形式有两种:两种形式都是将 callback 保留在一个援用对象中,区别就在于,形式 2 不须要去批改子组件,然而也减少了了解的老本

  • 放弃 useCallback, 应用 useRef 钩子,用 ref.current 去援用箭头函数。应用 useRef 还须要对子组件回调的调用批改一下。
const Child = (props) => {console.log('Child render');
  return (
    <div>
      <button onClick={() => {props.callbackRef.current();
           }}
       >Child click</button>
    </div>
  )
}

const parent = () => {const [count, setCount] = useState(1);
  
  const cbRef = React.useRef(null);
  cbRef.current = () => {console.log(count);
  };

  const ChildMemo = React.useMemo(() => <Child callbackRef={cbRef} />,
    []);

  return (
    <div style={{ 
      width: '100vw', height: '100vh', 
      justifyContent: 'center', alignItems: 'center', 
      display: 'flex', 
      flexDirection: 'column' }}
     >
      parent count : {count}
      <button onClick={() => setCount(count + 1)}>click</button>
      {ChildMemo}
    </div>
  )
}
  • 自定义一个 hook,应用 useCallback 搭配 useRef
const useImmediateValue = (cb) => {const ref = useRef(cb);
  ref.current = cb;
  const val = useCallback(() => {ref.current();
  }, []);
  return val;
}

const Child = (props) => {console.log('Child render');
  return (
    <div>
      <button onClick={props.clickCallback}>Child click</button>
    </div>
  )
}

const parent = () => {const [count, setCount] = useState(1);
  
  const cb = useImmediateValue(() => {console.log(count);
  })

  const ChildMemo = React.useMemo(() => <Child clickCallback={cb} />,
    []);

  return (
    <div style={{ 
      width: '100vw', height: '100vh', 
      justifyContent: 'center', alignItems: 'center', 
      display: 'flex', 
      flexDirection: 'column' }}
     >
      parent count : {count}
      <button onClick={() => setCount(count + 1)}>click</button>
      {ChildMemo}
    </div>
  )
}
退出移动版