Cancelling requestAnimationRequest in a React component using hooks doesn't work

…衆ロ難τιáo~ 提交于 2020-07-22 06:06:13

问题


I am working on a progress bar (Eventually..) and I want to stop the animation (calling cancelAnimationRequest) when reaching a certain value (10, 100, ..., N) and reset it to 0.

However, with my current code, it resets to 0 but keeps running indefinitely. I think I might have something wrong in this part of the code:

setCount((prevCount) => {
    console.log('requestRef.current', requestRef.current, prevCount);
    
    if (prevCount < 10) return prevCount + deltaTime * 0.001;
    
    // Trying to cancel the animation here and reset to 0:
    cancelAnimationFrame(requestRef.current);

    return 0;
});

This is the whole example:

const Counter = () => {
  const [count, setCount] = React.useState(0);

  // Use useRef for mutable variables that we want to persist
  // without triggering a re-render on their change:
  const requestRef = React.useRef();
  const previousTimeRef = React.useRef();

  const animate = (time) => {
    if (previousTimeRef.current != undefined) {
      const deltaTime = time - previousTimeRef.current;

      // Pass on a function to the setter of the state
      // to make sure we always have the latest state:
      setCount((prevCount) => {
        console.log('requestRef.current', requestRef.current, prevCount);
        
        if (prevCount < 10) return prevCount + deltaTime * 0.001;
        
        // Trying to cancel the animation here and reset to 0:
        cancelAnimationFrame(requestRef.current);

        return 0;
      });
    }
    
    previousTimeRef.current = time;
    requestRef.current = requestAnimationFrame(animate);
  }

  React.useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(requestRef.current);
  }, []);

  return <div>{ Math.round(count) }</div>;
}

ReactDOM.render(<Counter />, document.getElementById('app'));
html {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

body {
  font-size: 60px;
  font-weight: 700;
  font-family: 'Roboto Mono', monospace;
  color: #5D9199;
  background-color: #A3E3ED;
}

.as-console-wrapper {
  max-height: 66px !important;
}
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

Code pen: https://codepen.io/fr-nevin/pen/RwrLmPd


回答1:


The main problem with your code is that you are trying to cancel an update that has already been executed. Instead, you can just avoid requesting that last update that you don't need. You can see the problem and a simple solution for that below:

const Counter = () => {
  const [count, setCount] = React.useState(0);
  const requestRef = React.useRef();
  const previousTimeRef = React.useRef(0);

  const animate = React.useCallback((time) => {
    console.log('       RUN:', requestRef.current);

    setCount((prevCount) => {  
      const deltaTime = time - previousTimeRef.current;   
      const nextCount = prevCount + deltaTime * 0.001;
      
      // We add 1 to the limit value to make sure the last valid value is
      // also displayed for one whole "frame":
      if (nextCount >= 11) {
        console.log('    CANCEL:', requestRef.current, '(this won\'t work as inteneded)');
        
        // This won't work:
        // cancelAnimationFrame(requestRef.current);
        
        // Instead, let's use this Ref to avoid calling `requestAnimationFrame` again:
        requestRef.current = null;
      }
      
      return nextCount >= 11 ? 0 : nextCount;
    });
    
    // If we have already reached the limit value, don't call `requestAnimationFrame` again:
    if (requestRef.current !== null) {
      previousTimeRef.current = time;
      requestRef.current = requestAnimationFrame(animate);
      console.log('- SCHEDULE:', requestRef.current);
    }     
  }, []);

  React.useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(requestRef.current);
  }, []);
  
  // This floors the value:
  // See https://stackoverflow.com/questions/7487977/using-bitwise-or-0-to-floor-a-number.
  return (<div>{ count | 0 } / 10</div>);
};

ReactDOM.render(<Counter />, document.getElementById('app'));
html {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

body {
  font-size: 60px;
  font-weight: 700;
  font-family: 'Roboto Mono', monospace;
  color: #5D9199;
  background-color: #A3E3ED;
}

.as-console-wrapper {
  max-height: 66px !important;
}
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

In any case, you are also updating the state many more times than actually needed, which you can avoid by using refs and the timestamp (time) provided by requestAnimationFrame to keep track of the current and next/target counter values. You are still going to call the requestAnimationFrame update function the same number of times, but you will only update the state (setCount(...)) once you know the change is going to be reflected in the UI.

const Counter = ({ max = 10, rate = 0.001, location }) => {
  const limit = max + 1;
  const [count, setCount] = React.useState(0);
  const t0Ref = React.useRef(Date.now());
  const requestRef = React.useRef();
  const targetValueRef = React.useRef(1);

  const animate = React.useCallback(() => {
    // No need to keep track of the previous time, store initial time instead. Note we can't 
    // use the time param provided by requestAnimationFrame to the callback, as that one won't
    // be reset when the `location` changes:
    const time = Date.now() - t0Ref.current;    
    const nextValue = time * rate;
    
    if (nextValue >= limit) {
      console.log('Reset to 0');
      setCount(0);
      return;
    }
    
    const targetValue = targetValueRef.current;      
    
    if (nextValue >= targetValue) {
      console.log(`Update ${ targetValue - 1 } -> ${ nextValue | 0 }`);      
      setCount(targetValue);
      targetValueRef.current = targetValue + 1;
    }
    
    requestRef.current = requestAnimationFrame(animate);
  }, []);
  
  React.useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    
    return () => cancelAnimationFrame(requestRef.current);
  }, []);

  React.useEffect(() => {
    // Reset counter if `location` changes, but there's no need to call `cancelAnimationFrame` .
    setCount(0);
    t0Ref.current = Date.now();
    targetValueRef.current = 1;    
  }, [location]);
  
  return (<div className="counter">{ count } / { max }</div>);
};

const App = () => {
  const [fakeLocation, setFakeLocation] = React.useState('/');
  
  const handleButtonClicked = React.useCallback(() => {
    setFakeLocation(`/${ Math.random().toString(36).slice(2) }`);
  }, []);

  return (<div>  
    <span className="location">Fake Location: { fakeLocation }</span>
    <Counter max={ 10 } location={ fakeLocation } />
    <button className="button" onClick={ handleButtonClicked }>Update Parent</button>
  </div>);
};

ReactDOM.render(<App />, document.getElementById('app'));
html {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

body {
  font-family: 'Roboto Mono', monospace;
  color: #5D9199;
  background-color: #A3E3ED;
}

.location {
  font-size: 16px;
}

.counter {
  font-size: 60px;
  font-weight: 700;
}

.button {
  border: 2px solid #5D9199;
  padding: 8px;
  margin: 0;
  font-family: 'Roboto Mono', monospace;
  color: #5D9199;
  background: transparent;
  outline: none;
}

.as-console-wrapper {
  max-height: 66px !important;
}
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>


来源:https://stackoverflow.com/questions/62653091/cancelling-requestanimationrequest-in-a-react-component-using-hooks-doesnt-work

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!