React styles update only after wheel event fulfilled

What I want to achieve is smoothly scaled div container while scrolling (using mouse wheel to be strict) so user can zoom in and out.

However, my styles are "applied" by the browser only either when I scroll really slow or scroll normally and then wait about 0.2 seconds (after that time the changes are "bunched up"). I would like for the changes to be visible even during "fast" scrolling, not at the end.

The element with listener:

<div onWheel={(event) => {
         console.log("wheeling"); // this console log fires frequently, 
                                  // and I want to update styles at the same rate
         changeZoom(event);
     }}
>
    <div ref={scaledItem}> // content div that will be scaled according to event.deltaY
        ... // contents
    </div>
</div>

My React code:

const changeZoom = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
    if (!scaledItem.current) return;
    const newZoom = parseFloat(scaledItem.current.style.scale) + event.deltaY * 0.001;
    console.log(newZoom); // logs as frequently as "wheeling" above
    setCurrentZoom(newZoom);
}, []);


useEffect(() => {
    if (!scaledItem.current) return;
    scaledItem.current.style.scale = currentZoom.toString();
}, [currentZoom]);

useEffect(() => {        // this is just for reproduction, needs to set initial scale to 1
    if (!scaledItem.current) return;
    scaledItem.current.style.scale = "1";
}, [])

What I have tried first was to omit all the React states, and edit scaledItem.current.style.scale directly from useCallback, but the changes took place in a bunch, after the wheeling events stopped coming. Then I moved zoom amount to currentZoom useState hook, but rerenders don’t help either.

Edit:
I have also tried adding EventListener inside useEffect directly to the DOM Node:

useEffect(() => {
    if (!scaledItemWrapper.current) return; // ref for wrapper of my scaled content
    const container = scaledItemWrapper.current;
    container.addEventListener("wheel", changeZoom);
    return () => {
        container.removeEventListener("wheel", changeZoom);
    };
}, [changeZoom]);

>Solution :

Instead of setting up multiple states and observing can you try using a single state below is a working example. Try this if this works

https://codesandbox.io/s/wonderful-cerf-69doe?file=/src/App.js:0-727

export default () => {
  const [pos, setPos] = useState({ x: 0, y: 0, scale: 1 });

  const changeZoom = (e) => {
    e.preventDefault();
    const delta = e.deltaY * -0.01;
    const newScale = pos.scale + delta;

    const ratio = 1 - newScale / pos.scale;

    setPos({
      scale: newScale,
      x: pos.x + (e.clientX - pos.x) * ratio,
      y: pos.y + (e.clientY - pos.y) * ratio
    });
  };

  return (
    <div onWheelCapture={changeZoom}>
      <img
        src="https://source.unsplash.com/random/300x300?sky"
        style={{
          transformOrigin: "0 0",
          transform: `translate(${pos.x}px, ${pos.y}px) scale(${pos.scale})`
        }}
      />
    </div>
  );
};

Leave a Reply