How to mix useCallback with useRef in functional components

Viewed 5705

I'm newish to React and am working on an infinite scroll component. Multiple components will use infinite scroll and need to be synchronized together (i.e., scrolling one element programmatically scrolls other components as well).

So I've created ScrollProvider which maintains scroll state among components (even if they're rerendered), and a lower level hook useScrollSync. useScrollState returns a ref and a handleScroll callback which modify the state in the scroll provider. That all works just fine. However, I separately want to measure a component's size. The example provided in by the React team shows a callback since that for sure will be executed once the component is mounted, and the element would not be null. The problem is that the div already has a ref from the useScrollSync hook.

The core question

If I wanted to measure my div in addition to using scroll sync on it, how do I assign both a callback ref AND other ref to it? Is there a pattern around this, given that an element can only have one div?

Some (simplified) code:

ScrollProvider

const ScrollContext = React.createCreateContext();

const ScrollProvider = ({initialScrollTop, initialScrollLeft}) => {
  const controlledElements = useRef(new Map());
  const scrollPositions = useRef({
    scrollTop: initialScrollTop, 
    scrollLeft: initialScrollLeft, 
    controllingElementKey: null
  });

  const register = (key, controlledElementRef) => {
    controlledElements.current.set(key, controlledElementRef);
  }

  const handleScrollHOF = (key) => { 
    return () => { 
      scrollPositions.controllingElementKey = key;
      //some scrolling logic
    }
  }

  return {register, scrollPositions, handleScrollHOF};

}

useScrollSync

const useScrollSync = () => { 
  const scrollContext = useContext(ScrollContext);

  const elementRef = useRef(null);
  const keyRef = useRef({key: Symbol()}); // this probably could also be useState

  useEffect(() => {
    scrollContext.register(keyRef, elementRef);
  }, []);

return {ref: elementRef, handleScroll: handleScrollHOF(keyRef.current)};
}

SomeComponent (Round 1)

const SomeComponent = () => {
  // this would be within the provider tree
  const {ref, handleScroll} = useScrollSync();

  return (
    <div onScroll={handleScroll} ref={ref}>some stuff</div>
  )
}

Now the challenge is adding in a measurements hook...

useMeasurements

const useMeasurements = () => {
  // something like this, per the React team's Hooks FAQ
  const [measurements, setMeasurements] = useState(null);

  const measurementRef = useCallback((element) => {
    if(element !== null) {
      setMeasurements(element.getBoundingClientRect());
    }
  });

  return {measurementRef, measurements};
}

In order to add this to SomeComponent... SomeComponent (Round 2)

const SomeComponent = () => {
  // this would be within the provider tree
  const {ref, handleScroll} = useScrollSync();
  const {measurementRef, measurements} = useMeasurements();
  // I cannot assign measurementRef to this same div, 
  // and changing useMeasurements to just measure an injected ref winds up
  // with that ref being null and it never being recalculated

  return (
    <div onScroll={handleScroll} ref={ref}>some stuff</div>
  )
}

I've sort of hit a wall here, or maybe I'm just overtired. Any thoughts on how to get beyond this?

1 Answers

The main issue I see is that you aren't referencing a viable ref in useMeasurement. In addition, useCallback executes synchronously as part of rendering, before the DOM is created. You'll need to reference your element in another useEffect hook.

Related