skip to Main Content

When update the default time with setInterval, it’s not working as expected. Instead it’s added as a new instance. How to clear the setInterval in the custom hook and update new value?

app.jsx

import React from 'react';
import './style.css';
import CustomTimer from './Custom';
import { useState, useEffect } from 'react';

export default function App() {
  const [intervalTime, setIntervalTime] = useState(200);

  const time = CustomTimer(intervalTime);

  useEffect(() => {
    setTimeout(() => {
      console.log('Hi');
      setIntervalTime(500);
    }, 5000);
  });

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some happen! {time} </h2>
    </div>
  );
}

Custom.js

import { useEffect, useState } from 'react';

function CustomTimer(startTime) {
  const [timer, setTimer] = useState(startTime);
  useEffect(() => {
    const myInterval = setInterval(() => {
      if (timer > 0) {
        setTimer(timer - 1);
        console.log(timer);
      }
    }, 1000);
    return () => clearInterval(myInterval);
  }, [startTime]);
  return timer;
}

export default CustomTimer;

Live Demo => please check the console

2

Answers


  1. Issues

    This is the classic stale closure of state in a callback problem, with some additional discrepancies.

    • timer in the setInterval callback is the same value from the initial render cycle, so it always decrements from the same value.
    • The useEffect hook in CustomTimer doesn’t "reset" the timer state when the startTime prop value is changed.
    • The useEffect hook in App is missing a dependency array so it starts a new timeout each render cycle after intervalTime is updated the first time.
    • CustomTimer is really a custom React hook, it should be correctly renamed to useCustomTimer.

    Solution

    I suggest using a React ref to hold a reference to the current timer state value, to be used in the setInterval callback. This is to check if the timer is still valid. Use a functional state update to enqueue timer state updates.

    Example:

    import { useEffect, useRef, useState } from "react";
    
    function useCustomTimer(startTime) {
      const [timer, setTimer] = useState(startTime);
      const timerRef = useRef(timer);           // <-- ref to hold current timer value
    
      useEffect(() => {
        timerRef.current = timer;               // <-- keep ref synchronized to state
      }, [timer]);
    
      useEffect(() => {
        setTimer(startTime);                    // <-- reset timer when startTime changes
    
        const myInterval = setInterval(() => {
          if (timerRef.current > 0) {           // <-- use timer ref for check
            setTimer((timer) => timer - 1);     // <-- functional state update
            console.log(timerRef.current);
          } else {
            console.log("Timer expired");
            clearInterval(myInterval);          // <-- clear when expired
          }
        }, 1000);
        return () => clearInterval(myInterval); // <-- clear on startTime update or umounting
      }, [startTime]);
    
      return timer;
    }
    
    export default function App() {
      const [intervalTime, setIntervalTime] = useState(200);
    
      const time = useCustomTimer(intervalTime);
    
      useEffect(() => {
        setTimeout(() => {
          console.log("Hi");
          setIntervalTime(500);
        }, 5000);
      }, []); // <-- add missing dependency array
    
      return (
        <div className="App">
          <h1>Hello CodeSandbox</h1>
          <h2>Start editing to see some magic happen! {time}</h2>
        </div>
      );
    }
    

    Demo

    Edit custom-hook-with-clear-and-update-time-not-works-as-per-expected

    Login or Signup to reply.
  2. The results can be tested here as well ==> Test Demo

    First of all, need to add a dependency to the useEffect in the App.js file since whenever it triggers, the timer starts from the beginning.

    App.js:

    import React from 'react';
    import './style.css';
    import CustomTimer from './Custom';
    import { useState, useEffect } from 'react';
    
    export default function App() {
      const [intervalTime, setIntervalTime] = useState(200);
      const time = CustomTimer(intervalTime);
    
      useEffect(() => {
        setTimeout(() => {
          setIntervalTime(500);
        }, 5000);
      }, []);  // Added dependency array here
    
      return (
        <div className="App">
          <h1>Hello CodeSandbox</h1>
          <h2>Start editing to see some happen! {time} </h2>
        </div>
      );
    }
    

    Custom.js: (Added comment lines to explain the added/updated lines)

    import { useEffect, useState } from 'react';
    
    function CustomTimer(startTime) {
      const [timer, setTimer] = useState(startTime);
    
      useEffect(() => {
        setTimer(startTime);
        let timerCounter = startTime;
    
        const myInterval = setInterval(() => {
          if (timerCounter > 0) {
            // Get the previous state and decrease it 
            setTimer(timer => timer - 1);
            timerCounter--;
            return;
          }
    
          // Clear interval after timer's work is done
          clearInterval(myInterval);
        }, 1000);
    
        return () => clearInterval(myInterval);
      }, [startTime]);
    
      return timer;
    }
    
    export default CustomTimer;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search