pmndrs / react-use-measure

🙌 Utility to measure view bounds
MIT License
843 stars 30 forks source link

bug: react 18 out of sync state updates #93

Open dontsave opened 1 year ago

dontsave commented 1 year ago

hi there. just wanted to flag a little bug with setting state from ResizeObserver callbacks that happens in React 18 due to setState batching. we are seeing this issue surface in react-use-measure currently. the solution is to wrap the internal setState influshSync from ReactDOM:

https://github.com/facebook/react/issues/24331

there may be performance ramifications in doing this, so it might be a good thing to have as opt-in

donaldpipowitch commented 9 months ago

I workaround this by manually setting an initial rect:

const betterContainerRef = useCallback((element: HTMLDivElement | null) => {
    if (element) setInitialRect(element.getBoundingClientRect());
    containerRef(element); // from react-use-measure
  }, []);

I use the initial rect whenever the measured rect is 0. This helped to avoid the visual glitch for me.

donaldpipowitch commented 3 weeks ago

FYI: I use this now:

// inspired by https://github.com/streamich/react-use/blob/master/src/useMeasure.ts
import { useLayoutEffect, useMemo, useState } from 'react';

export type RectReadOnly = Pick<
  DOMRectReadOnly,
  'x' | 'y' | 'top' | 'left' | 'right' | 'bottom' | 'height' | 'width'
>;

const defaultState: RectReadOnly = {
  x: 0,
  y: 0,
  width: 0,
  height: 0,
  top: 0,
  left: 0,
  bottom: 0,
  right: 0,
};

export function useMeasure() {
  const [element, ref] = useState<HTMLDivElement | null>(null);
  const [rect, setRect] = useState(defaultState);

  const observer = useMemo(
    () =>
      new ResizeObserver((entries) => {
        if (entries[0]) {
          setRect(entries[0].target.getBoundingClientRect());
        }
      }),
    []
  );

  useLayoutEffect(() => {
    if (!element) return;
    setRect(element.getBoundingClientRect());
    observer.observe(element);
    return () => {
      observer.disconnect();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [element]);

  return [ref, rect] as const;
}