Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React hook useState's setter inside function doesn't work

I'm trying to do a refactor of a countdown component that a project I'm working on has.

When I finished the migration of the logic the value of the counter didn't work. I decided to start from zero in codesandbox, so I tought of the simplest implementation and came out with this:

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

import "./styles.css";

function App() {
  const [counter, setCounter] = useState(60);

  useEffect(() => {
    const interval = setInterval(() => setCounter(counter - 1), 1000);

    return () => clearInterval(interval);
  }, []);

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

const rootElement = document.getElementById("root");
ReactDOM.render(<App timerSeconds={360} />, rootElement);

What it happens here is that the value of counter stays on 59 after the first run of the interval.

Codesandbox: https://codesandbox.io/embed/flamboyant-moon-ogyqr

Second iteration on issue

Thank you for the response Ross, but the real issue happens when I link the countdown to a handler:

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

import "./styles.css";

function App() {
  const [counter, setCounter] = useState(60);
  const [countdownInterval, setCountdownInterval] = useState(null);


  const startCountdown = () => {
    setCountdownInterval(setInterval(() => setCounter(counter - 1), 1000));
  };

  useEffect(() => {
    return () => clearInterval(countdownInterval);
  });

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

const rootElement = document.getElementById("root");
ReactDOM.render(<App timerSeconds={360} />, rootElement);

like image 823
MikeVelazco Avatar asked May 20 '26 19:05

MikeVelazco


2 Answers

Add the counter variable within the second parameter (the array) of the useEffect function. When you pass in an empty array, it will only update the state once on the initial render. An empty array is often used when you're making an HTTP request or something along those lines instead. (Edited for second iteration)

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

import "./styles.css";

function App() {
  const [counter, setCounter] = useState(5);
  const [counterId, setCounterId] = useState(null);

  useEffect(() => {
    return () => clearInterval(counterId);
  }, []);

  const handleClick = () => {

    /* 
     * I'd take startCountdown and make
     * it's own component/hook out of it, 
     * so it can be easily reused and expanded.
     */
    const startCountdown = setInterval(() => {
      return setCounter((tick) => {
        if (tick === 0) {
          clearInterval(counterId);
          setCounter(0);
          return setCounterId(null);
        };

        return tick - 1;
      });
    }, 1000)

    setCounterId(startCountdown);
  };

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

const rootElement = document.getElementById("root");
ReactDOM.render(<App timerSeconds={360} />, rootElement);

For more information on this implementation, read about React Hooks and skipping effects at https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects.

like image 150
Ross Sheppard Avatar answered May 23 '26 16:05

Ross Sheppard


You can use the Functional Updates version of the function returned by useState to compute the new state based on the previous state.

Your updated code would look like this:

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

import "./styles.css";

function App() {
  const [counter, setCounter] = useState(60);

  useEffect(() => {
    const interval = setInterval(() => {setCounter(counter => counter - 1);}, 1000);

    return () => clearInterval(interval);
  }, []);

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

const rootElement = document.getElementById("root");
ReactDOM.render(<App timerSeconds={360} />, rootElement);

UPDATE EDIT Here is a version that starts the countdown with a click handler:

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

import "./styles.css";

function App() {
  const [counter, setCounter] = useState(60);
  const [countdownInterval, setCountdownInterval] = useState(null);


  const startCountdown = () => {
    setCountdownInterval(setInterval(() => setCounter(counter => counter - 1), 1000));
  };

  useEffect(() => {
    return () => clearInterval(countdownInterval);
  }, [countdownInterval]);

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

const rootElement = document.getElementById("root");
ReactDOM.render(<App timerSeconds={360} />, rootElement);
like image 23
jmclellan Avatar answered May 23 '26 14:05

jmclellan