Home > Software design >  React styles update only after wheel event fulfilled
React styles update only after wheel event fulfilled

Time:07-27

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]);

CodePudding user response:

I got your code working pretty much without changes. I only had to change scale because it has poor browser support. Perhaps some other code not posted here is causing the behavior you observe?

I see that if you just set scale directly, it always gives choppy behavior, making the scale jump instantly according to how much distance was traveled before the onWheel event.

I'm not sure if this is precisely what you're asking, but it might still help. You can add a CSS transition on the transform property to smooth out the scaling movement. It seems to work well with a value of .2 seconds for this case.

transition: transform .2s ease-out;

For best layout, it's best to first run the snippet before opening it full page. Otherwise it works but it will still scroll the whole page.

Without transition (choppy)

const {useCallback, useEffect, useState, useRef} = React;

const minZoom = .01;

function App() {
  const [currentZoom, setCurrentZoom] = useState("1");
  const scaledItem = useRef();

  const changeZoom = useCallback((event) => {
    if (!scaledItem.current) return;
    const scaleNumber = scaledItem.current.style.transform.replace('scale(','').replace(')','');
    const newZoom = Math.max(minZoom, parseFloat(scaleNumber)   event.deltaY * 0.001);
    console.log(newZoom); // logs as frequently as "wheeling" above
    setCurrentZoom(newZoom);
  }, []);


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

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

  
  return <div onWheel={(event) => {
         console.log("wheeling"); 
         changeZoom(event);
     }}
  >
    <div  ref={scaledItem}>
      <p>Scale me up and down!</p>
    </div>
  </div>
}

ReactDOM.render(<App/>, document.getElementById('root'));
.scaled {
  border: 2px solid lightgreen;
  transform: scale(1);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>

<div id="root"></div>

<div id="root2"></div>

With transition (smooth)

const {useCallback, useEffect, useState, useRef} = React;

const minZoom = .01;

function App() {
  const [currentZoom, setCurrentZoom] = useState("1");
  const scaledItem = useRef();

  const changeZoom = useCallback((event) => {
    if (!scaledItem.current) return;
    const scaleNumber = scaledItem.current.style.transform.replace('scale(','').replace(')','');
    const newZoom = Math.max(minZoom, parseFloat(scaleNumber)   event.deltaY * 0.001);
    console.log(newZoom); // logs as frequently as "wheeling" above
    setCurrentZoom(newZoom);
  }, []);


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

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

  
  return <div onWheel={(event) => {
         console.log("wheeling"); 
         changeZoom(event);
     }}
  >
    <div  ref={scaledItem}>
      <p>Scale me up and down!</p>
    </div>
  </div>
}

ReactDOM.render(<App/>, document.getElementById('root'));
.scaled {
  border: 2px solid lightgreen;
  transform: scale(1);
  transition: transform .2s ease-out;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>

<div id="root"></div>

<div id="root2"></div>

CodePudding user response:

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>
  );
};
  • Related