136

I'm still fairly new at React, but I've been grinding along slowly and I've encountered something I'm stuck on.

I am trying to build a "timer" component in React, and to be honest I don't know if I'm doing this right (or efficiently). In my code below, I set the state to return an object { currentCount: 10 } and have been toying with componentDidMount, componentWillUnmount, and render and I can only get the state to "count down" from 10 to 9.

Two-part question: What am I getting wrong? And, is there a more efficient way of going about using setTimeout (rather than using componentDidMount & componentWillUnmount)?

Thank you in advance.

import React from 'react';

var Clock = React.createClass({

  getInitialState: function() {
    return { currentCount: 10 };
  },

  componentDidMount: function() {
    this.countdown = setInterval(this.timer, 1000);
  },

  componentWillUnmount: function() {
    clearInterval(this.countdown);
  },

  timer: function() {
    this.setState({ currentCount: 10 });
  },

  render: function() {
    var displayCount = this.state.currentCount--;
    return (
      <section>
        {displayCount}
      </section>
    );
  }

});

module.exports = Clock;
Jose
  • 4,880
  • 8
  • 27
  • 49
  • 2
    `bind(this)` is no longer needed, react does this on its own now. – Derek Pollard Mar 30 '16 at 03:36
  • 2
    your timer method does not update currentCount – Bryan Chen Mar 30 '16 at 03:37
  • 2
    @Derek are you sure? I just got mine working by adding `this.timer.bind(this)` as this.timer on it's own did not work – The worm Nov 23 '16 at 10:51
  • 6
    @Theworm @Derek is wrong, kind of. React.createClass (which is deprecated) autobinds methods, but `class Clock extends Component` doesn't auto bind. So it depends on how you're creating your components whether you need to bind. – CallMeNorm Jan 10 '17 at 18:54

11 Answers11

184

I see 4 issues with your code:

  • In your timer method you are always setting your current count to 10
  • You try to update the state in render method
  • You do not use setState method to actually change the state
  • You are not storing your intervalId in the state

Let's try to fix that:

componentDidMount: function() {
   var intervalId = setInterval(this.timer, 1000);
   // store intervalId in the state so it can be accessed later:
   this.setState({intervalId: intervalId});
},

componentWillUnmount: function() {
   // use intervalId from the state to clear the interval
   clearInterval(this.state.intervalId);
},

timer: function() {
   // setState method is used to update the state
   this.setState({ currentCount: this.state.currentCount -1 });
},

render: function() {
    // You do not need to decrease the value here
    return (
      <section>
       {this.state.currentCount}
      </section>
    );
}

This would result in a timer that decreases from 10 to -N. If you want timer that decreases to 0, you can use slightly modified version:

timer: function() {
   var newCount = this.state.currentCount - 1;
   if(newCount >= 0) { 
       this.setState({ currentCount: newCount });
   } else {
       clearInterval(this.state.intervalId);
   }
},
dotnetom
  • 24,551
  • 9
  • 51
  • 54
  • Thank you. This makes a lot of sense. I'm still very much a beginner and I'm trying to get a hold of how state works and what goes in which "chunks", like render. – Jose Mar 30 '16 at 03:47
  • I am wondering, though, is it necessary to use componentDidMount and componentWillUnmount to actually set the interval? EDIT: Just saw your most recent edit. :) – Jose Mar 30 '16 at 03:49
  • @Jose I think `componentDidMount` is the right place to trigger the client side events, so I would use it to initiate the countdown. What other method are you thinking about for initializing? – dotnetom Mar 30 '16 at 03:52
  • I didn't have anything else in particular in mind, but it seemed clunky to use so many "chunks" inside a component. I suppose it is just myself getting accustomed to how the bits and pieces work in React. Again, thank you! – Jose Mar 30 '16 at 03:54
  • @dotnetom why would mine be potentially failing saying cannot read currentCount of undefined in the timer method. I have mine set up exactly the same as yours just have the ES6 way of doing getinitialstate. i.e. constructor(props) { super(props); this.state = { currentCount: 10 }; }; – The worm Nov 23 '16 at 10:10
  • @Theworm, first thing that comes to mind is issues with binding (ES6 does not bind `this` automatically) – dotnetom Nov 23 '16 at 18:19
  • How come timer gets proper context when invoked by `setInterval` ? – bigopon Apr 12 '17 at 11:43
  • 7
    There's no real need to store the setInterval value as part of the state because it doesn't affect the rendering – Gil Feb 21 '18 at 14:54
  • 1
    In ES6, don't forget to bind the class to access this : `this.timer.bind(this)` and `timer() { ... }` – Vincent Decaux Mar 30 '18 at 13:18
37

Updated 10-second countdown using class Clock extends Component

import React, { Component } from 'react';

class Clock extends Component {
  constructor(props){
    super(props);
    this.state = {currentCount: 10}
  }
  timer() {
    this.setState({
      currentCount: this.state.currentCount - 1
    })
    if(this.state.currentCount < 1) { 
      clearInterval(this.intervalId);
    }
  }
  componentDidMount() {
    this.intervalId = setInterval(this.timer.bind(this), 1000);
  }
  componentWillUnmount(){
    clearInterval(this.intervalId);
  }
  render() {
    return(
      <div>{this.state.currentCount}</div>
    );
  }
}

module.exports = Clock;
Greg Herbowicz
  • 1,210
  • 1
  • 17
  • 18
35

Updated 10-second countdown using Hooks (a new feature proposal that lets you use state and other React features without writing a class. They’re currently in React v16.7.0-alpha).

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

const Clock = () => {
    const [currentCount, setCount] = useState(10);
    const timer = () => setCount(currentCount - 1);

    useEffect(
        () => {
            if (currentCount <= 0) {
                return;
            }
            const id = setInterval(timer, 1000);
            return () => clearInterval(id);
        },
        [currentCount]
    );

    return <div>{currentCount}</div>;
};

const App = () => <Clock />;

ReactDOM.render(<App />, document.getElementById('root'));
Greg Herbowicz
  • 1,210
  • 1
  • 17
  • 18
26

If anyone is looking for a React Hook approach to implementing setInterval. Dan Abramov talked about it on his blog. Check it out if you want a good read about the subject including a Class approach. Basically the code is a custom Hook that turns setInterval as declarative.

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

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

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

Also posting the CodeSandbox link for convenience: https://codesandbox.io/s/105x531vkq

Jo E.
  • 7,822
  • 14
  • 58
  • 94
10

Manage setInterval with React Hooks:

  const [seconds, setSeconds] = useState(0)

  const interval = useRef(null)

  useEffect(() => { if (seconds === 60) stopCounter() }, [seconds])

  const startCounter = () => interval.current = setInterval(() => {
    setSeconds(prevState => prevState + 1)
  }, 1000)

  const stopCounter = () => clearInterval(interval.current)
2

Thanks @dotnetom, @greg-herbowicz

If it returns "this.state is undefined" - bind timer function:

constructor(props){
    super(props);
    this.state = {currentCount: 10}
    this.timer = this.timer.bind(this)
}
tulsluper
  • 1,688
  • 1
  • 13
  • 8
2

If you are using Dan Abramov useInterval hook and want to manually cancel the current interval you just need to call the hook again passing null as delay variable.

You can check a working example here https://codesandbox.io/s/useinterval-cancel-interval-dan-abramov-extended-oe45z?file=/src/index.js

Juanma Menendez
  • 17,253
  • 7
  • 59
  • 56
1

The easy thing to do is to add it to the window variable.

useEffect(() => {
    window.interval23 = setInterval(
      () => setState('something'),
      2500
    )
    return () => {
      clearInterval(window.interval23)
    }
 }, [])

but make sure whatever you create with the window variable, keep it as unique as possible because the window variable might interrupt in libraries if that variable already exists.

1

You can use interval to set state by creating what I call a fake recursion by combining setTimeout and useEffect

  import {useEffect,useState} from 'react'

  const [state,setState]=useState(0)

  function Interval(){
      setTimeout(()=>{
         setState(state+1)
       },2000)
  }
   useEffect(()=>Interval(),[state])

 //this code runs repeatedly in interval of 2 seconds
Cristik
  • 30,989
  • 25
  • 91
  • 127
0

Updating state every second in the react class. Note the my index.js passes a function that return current time.

import React from "react";

class App extends React.Component {
  constructor(props){
    super(props)

    this.state = {
      time: this.props.time,

    }        
  }
  updateMe() {
    setInterval(()=>{this.setState({time:this.state.time})},1000)        
  }
  render(){
  return (
    <div className="container">
      <h1>{this.state.time()}</h1>
      <button onClick={() => this.updateMe()}>Get Time</button>
    </div>
  );
}
}
export default App;
Ashok Shah
  • 69
  • 1
  • 1
-1
import React, { useState, useEffect } from "react";

export const Count = () => {
const [currentCount, setCount] = useState(1);

const timer = () => setCount(currentCount + 1);

useEffect(
    () => {
        if (currentCount <= 0) {
            return;
        }
        const id = setInterval(timer, 1000);
        return () => clearInterval(id);
    },
    [currentCount]
);

console.log(currentCount)

return <div>Count : - {currentCount}</div>;
};
Deepak Singh
  • 749
  • 4
  • 16