React useEffectEvent

React useEffectEvent
Photo by Ferenc Almasi / Unsplash

在 React 最新的实验版中引入了一个新的 useEffectEvent hook, 引入这个新的 hook 出发点是为了解决下面场景中的问题:

function View() {
 const [data, setData] = useState<{timestamp: number, value: any}>()
 
 const listener = useEffectEvent((newData) => {
    if (!data.timestamp || after(data.timestamp, 5min)) {
      setData(newData)
    }
 })

 useEffect(() => {
   // effect to subscribe some remote data
   remote.subscribe('data', listener)
   return () => remote.unsubscribe()
 }, [])

 return (
   <div>{data}</div>
 )
}

listener 中我们可以拿到所有reactive value 的最新 snapshot ,上面的例子里是 data .而通常我们的做法可能是,将listener 封装在 useEffect 中然后监听 data 的变化:

function View() {
 const [data, setData] = useState<{timestamp: number, value: any}>()

 useEffect(() => {
   const listener = (newData) => {
     if (!data.timestamp || after(data.timestamp, 5min)) {
       setData(newData)
     }
   }
   // effect to subscribe some remote data
   remote.subscribe('data', listener)
   return () => remote.unsubscribe()
 }, [data])

 return (
   <div>{data}</div>
 )
}

这种实现的潜在问题是当 data 更新时, useEffect 里的函数会被不断执行。上面的例子中在第一次执行后,以后每次 data 更新时都会经过先unsubscribe 然后再 subscribe, 这时会造成不必要的 IO 操作,甚至造成race condition。

为什么下面的代码不行呢:

function View() {
 const [data, setData] = useState<{timestamp: number, value: any}>()
 const listener = useCallback((newData) => {
   if (!data.timestamp || after(data.timestamp, 5min)) {
     setData(newData)
   }
 }, [data])
 useEffect(() => {
   // effect to subscribe some remote data
   remote.subscribe('data', listener)
   return () => remote.unsubscribe()
 }, [])

 return (
   <div>{data}</div>
 )
}

因为在组件 View 第一次渲染的时候 useEffect 里拿到的也是 useCallback 第一次执行后的snapshot,以后每次 useCallback 重新执行后的数据对 useEffect 来说都是不可见的。

useEffectEvent 的简单实现:

const useEffectEvent = (fn) => {
  const ref = useRef(fn)
  ref.current = fn
  return (...args) => ref.current(...args)
}
function View() {
 const [data, setData] = useState<{timestamp: number, value: any}>()
 const listener = useEffectEvent((newData) => {
   if (!data.timestamp || after(data.timestamp, 5min)) {
     setData(newData)
   }
 })
 useEffect(() => {
   // effect to subscribe some remote data
   remote.subscribe('data', listener)
   return () => remote.unsubscribe()
 }, [data])

 return (
   <div>{data}</div>
 )
}

其实原理就是将 listener 这个函数从 immutable 变成一个mutable的函数,每次执行的时候都可以读到所有 reactive 数据的最新数据。这么做的问题在于我们并不能保证组件 View 在多次渲染的情况得到相同的结果,这时候父级组件就会认为需要重新渲染更新DOM。

See more: