Matt K
Matt K

Reputation: 4948

react hooks: how to handle co-dependent useCallbacks

the react hook's linter likes to be strict with the DependencyList. That leads to the following broken situation where 2 event handlers depend on each other. Since the functions are registered with addEventListener I know if they ever change it'll introduce a memory leak so the easy thing to do is just empty the dependency list-- but what is the right way to handle this while playing by the linter's rules?

const onMouseMove = useCallback((e) => {
  if (!isSwipe(e)) {
    onMouseUp(e)
  }
}, [onMouseUp])

const onMouseUp = useCallback((e) => {
  document.removeEventListener('mousemove', onMouseMove)
}, [onMouseMove])

Upvotes: 9

Views: 721

Answers (2)

Retsam
Retsam

Reputation: 33389

useCallback is essentially a version of useMemo, specialized for memoizing functions. If two functions are co-dependent and can't be memoized separately with useCallback, they can be memoized together with useMemo:

const { onMouseMove, onMouseUp } = useMemo(() => {
    const onMouseMove = (e) => {
        if (!isSwipe(e)) {
            onMouseUp(e)
        }
    };
    const onMouseUp = (e) => {
        document.removeEventListener('mousemove', onMouseMove)
    }
    return { onMouseMove, onMouseUp };
}, [/* shared deps*/]);

This is the general pattern I'd use to memoize functions together, and I think it's the simplest answer to the general question. But I'm not sure, looking at the actual code here, that it's the best approach - the removeEventListener may not work if the onMouseMove is a different function than the one that was registered due to useMemo recalculating. Might be more of a useEffect sort of use-case, in practice.

Upvotes: 5

Matt K
Matt K

Reputation: 4948

after some research, looks like the solution for (useCallback() invalidates too often in practice) also fixes this. The general idea is to memoize a function that points to a ref that points to the most recent function.

It's a dirty hack that may also cause problems in concurrent mode, but for now, it's what facebook recommends:How to read an often-changing value from useCallback

const useEventCallback = (fn) => {
    const ref = useRef(fn)
    useEffect(() => {
        ref.current = fn
    })
    return useCallback((...args) => {
        ref.current(...args)
    }, [ref])
}

const onMouseMove = useEventCallback((e) => {
  if (!isSwipe(e)) {
    onMouseUp(e)
  }
})

const onMouseUp = useEventCallback((e) => {
  document.removeEventListener('mousemove', onMouseMove)
})

Upvotes: 0

Related Questions