admin管理员组

文章数量:1026900

I'm currently building a timer in ReactJS for practice. Currently I have two ponents, a Timer ponent which displays the time as well as sets an interval upon mounting. I also have an App ponent which keeps tracks of the main state, such as state for whether the timer is paused as well as the current value of the timer.

My goal is to make it so that when I click the pause button, the timer stops incrementing. Currently I've tried to achieve this by using:

if(!paused) 
  tick(time => time+1);

which in my mind, should only increment the time state when paused is false. However, when I update the paused state by clicking on my button, this paused state inside the setTimeout does not change. My guess that setTimeout is forming a closure over the paused state, so it's not updating when the state changes. I tried adding paused as a dependency to useEffect but this caused multiple timeouts to be queued whenever paused changed.

const {useState, useEffect} = React;

const Timer = ({time, paused, tick}) => {
  useEffect(() => {
    const timer = setInterval(() => {
      if(!paused) // `paused` doesn't change?
        tick(time => time+1);
    }, 1000);
  }, []);
  
  return <p>{time}s</p>
}

const App = () => {
  const [time, setTime] = useState(0);
  const [paused, setPaused] = useState(false);
  const togglePaused = () => setPaused(paused => !paused);
  
  return (
    <div>
      <Timer paused={paused} time={time} tick={setTime} />
      <button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
    </div>
  );
}

ReactDOM.render(<App />, document.body);
<script src=".8.4/umd/react.production.min.js"></script>
<script src=".8.4/umd/react-dom.production.min.js"></script>

I'm currently building a timer in ReactJS for practice. Currently I have two ponents, a Timer ponent which displays the time as well as sets an interval upon mounting. I also have an App ponent which keeps tracks of the main state, such as state for whether the timer is paused as well as the current value of the timer.

My goal is to make it so that when I click the pause button, the timer stops incrementing. Currently I've tried to achieve this by using:

if(!paused) 
  tick(time => time+1);

which in my mind, should only increment the time state when paused is false. However, when I update the paused state by clicking on my button, this paused state inside the setTimeout does not change. My guess that setTimeout is forming a closure over the paused state, so it's not updating when the state changes. I tried adding paused as a dependency to useEffect but this caused multiple timeouts to be queued whenever paused changed.

const {useState, useEffect} = React;

const Timer = ({time, paused, tick}) => {
  useEffect(() => {
    const timer = setInterval(() => {
      if(!paused) // `paused` doesn't change?
        tick(time => time+1);
    }, 1000);
  }, []);
  
  return <p>{time}s</p>
}

const App = () => {
  const [time, setTime] = useState(0);
  const [paused, setPaused] = useState(false);
  const togglePaused = () => setPaused(paused => !paused);
  
  return (
    <div>
      <Timer paused={paused} time={time} tick={setTime} />
      <button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
    </div>
  );
}

ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

So, my question is:

  1. Why isn't my current code working (why is paused not updating within useEffect)?
  2. Is there any way to make the above code work so that I can "pause" my interval which is set within the useEffect()?
Share Improve this question asked Jul 31, 2020 at 13:15 ShnickShnick 1,4111 gold badge18 silver badges39 bronze badges
Add a ment  | 

3 Answers 3

Reset to default 4

To stop the timer return the function clearing the interval from useEffect().

React performs the cleanup when the ponent unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.

(source: Using the Effect Hook - Effects with Cleanup)

You should also pass the paused in dependencies array to stop useEffect creating new intervals on each re-render. If you add [paused] it'll only create new interval when paused change.

const {useState, useEffect} = React;

const Timer = ({time, paused, tick}) => {
  useEffect(() => {
    const timer = setInterval(() => {
      if(!paused) // `paused` doesn't change?
        tick(time => time+1);
    }, 1000);
    
    return () => clearInterval(timer);
  }, [paused]);
 
  return <p>{time}s</p>
}

const App = () => {
  const [time, setTime] = useState(0);
  const [paused, setPaused] = useState(false);
  const togglePaused = () => setPaused(paused => !paused);
  
  return (
    <div>
      <Timer paused={paused} time={time} tick={setTime} />
      <button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

The setInterval captures the paused value the first time, you'll need to remove the interval, recreate it everytime paused is changed.

You can check this article for more info: https://overreacted.io/making-setinterval-declarative-with-react-hooks

You can use Dan's useInterval hook. I can't explain it better than him why you should use this.

The hook looks like this.

function useInterval(callback, delay) {
  const savedCallback = React.useRef();

  // Remember the latest callback.
  React.useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  React.useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

Then use it in your Timer ponent like this.

const Timer = ({ time, paused, tick }) => {
  useInterval(() => {
    if (!paused) tick(time => time + 1);
  }, 1000);

  return <p>{time}s</p>;
};

I believe the difference is that useInterval hook is aware of its dependencies (paused) while setInterval isn't.

I'm currently building a timer in ReactJS for practice. Currently I have two ponents, a Timer ponent which displays the time as well as sets an interval upon mounting. I also have an App ponent which keeps tracks of the main state, such as state for whether the timer is paused as well as the current value of the timer.

My goal is to make it so that when I click the pause button, the timer stops incrementing. Currently I've tried to achieve this by using:

if(!paused) 
  tick(time => time+1);

which in my mind, should only increment the time state when paused is false. However, when I update the paused state by clicking on my button, this paused state inside the setTimeout does not change. My guess that setTimeout is forming a closure over the paused state, so it's not updating when the state changes. I tried adding paused as a dependency to useEffect but this caused multiple timeouts to be queued whenever paused changed.

const {useState, useEffect} = React;

const Timer = ({time, paused, tick}) => {
  useEffect(() => {
    const timer = setInterval(() => {
      if(!paused) // `paused` doesn't change?
        tick(time => time+1);
    }, 1000);
  }, []);
  
  return <p>{time}s</p>
}

const App = () => {
  const [time, setTime] = useState(0);
  const [paused, setPaused] = useState(false);
  const togglePaused = () => setPaused(paused => !paused);
  
  return (
    <div>
      <Timer paused={paused} time={time} tick={setTime} />
      <button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
    </div>
  );
}

ReactDOM.render(<App />, document.body);
<script src=".8.4/umd/react.production.min.js"></script>
<script src=".8.4/umd/react-dom.production.min.js"></script>

I'm currently building a timer in ReactJS for practice. Currently I have two ponents, a Timer ponent which displays the time as well as sets an interval upon mounting. I also have an App ponent which keeps tracks of the main state, such as state for whether the timer is paused as well as the current value of the timer.

My goal is to make it so that when I click the pause button, the timer stops incrementing. Currently I've tried to achieve this by using:

if(!paused) 
  tick(time => time+1);

which in my mind, should only increment the time state when paused is false. However, when I update the paused state by clicking on my button, this paused state inside the setTimeout does not change. My guess that setTimeout is forming a closure over the paused state, so it's not updating when the state changes. I tried adding paused as a dependency to useEffect but this caused multiple timeouts to be queued whenever paused changed.

const {useState, useEffect} = React;

const Timer = ({time, paused, tick}) => {
  useEffect(() => {
    const timer = setInterval(() => {
      if(!paused) // `paused` doesn't change?
        tick(time => time+1);
    }, 1000);
  }, []);
  
  return <p>{time}s</p>
}

const App = () => {
  const [time, setTime] = useState(0);
  const [paused, setPaused] = useState(false);
  const togglePaused = () => setPaused(paused => !paused);
  
  return (
    <div>
      <Timer paused={paused} time={time} tick={setTime} />
      <button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
    </div>
  );
}

ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

So, my question is:

  1. Why isn't my current code working (why is paused not updating within useEffect)?
  2. Is there any way to make the above code work so that I can "pause" my interval which is set within the useEffect()?
Share Improve this question asked Jul 31, 2020 at 13:15 ShnickShnick 1,4111 gold badge18 silver badges39 bronze badges
Add a ment  | 

3 Answers 3

Reset to default 4

To stop the timer return the function clearing the interval from useEffect().

React performs the cleanup when the ponent unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.

(source: Using the Effect Hook - Effects with Cleanup)

You should also pass the paused in dependencies array to stop useEffect creating new intervals on each re-render. If you add [paused] it'll only create new interval when paused change.

const {useState, useEffect} = React;

const Timer = ({time, paused, tick}) => {
  useEffect(() => {
    const timer = setInterval(() => {
      if(!paused) // `paused` doesn't change?
        tick(time => time+1);
    }, 1000);
    
    return () => clearInterval(timer);
  }, [paused]);
 
  return <p>{time}s</p>
}

const App = () => {
  const [time, setTime] = useState(0);
  const [paused, setPaused] = useState(false);
  const togglePaused = () => setPaused(paused => !paused);
  
  return (
    <div>
      <Timer paused={paused} time={time} tick={setTime} />
      <button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

The setInterval captures the paused value the first time, you'll need to remove the interval, recreate it everytime paused is changed.

You can check this article for more info: https://overreacted.io/making-setinterval-declarative-with-react-hooks

You can use Dan's useInterval hook. I can't explain it better than him why you should use this.

The hook looks like this.

function useInterval(callback, delay) {
  const savedCallback = React.useRef();

  // Remember the latest callback.
  React.useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  React.useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

Then use it in your Timer ponent like this.

const Timer = ({ time, paused, tick }) => {
  useInterval(() => {
    if (!paused) tick(time => time + 1);
  }, 1000);

  return <p>{time}s</p>;
};

I believe the difference is that useInterval hook is aware of its dependencies (paused) while setInterval isn't.

本文标签: javascriptState within useEffect not updatingStack Overflow