Build a 25 + 5 Clock useState setTimer lag

Tell us what’s happening:

I am trying to get the Session Increment button to update the Session Length and the Timer, but the Timer always ends up lagging one update behind the Session Length.

This is the Session Length function:

const pressSession = (event) => {
        if (event.target.id == 'session-increment') {
            if (session < 60) setSession(parseInt(session)+1);
        } else {
            if (session > 1) setSession(parseInt(session)-1);
        }
        setTimer(session + ":00")
    }

When clicking Session Length increment:

Start: Session 25:00, Length: 25
1st click: Session 25:00, Length: 26
2nd click: Session 26:00, Length: 27

Why doesn’t setTimer(session + ":00") immediately update timer and re-render?

I ran into the same problem updating the display with the Calculator and refactored everything but I’m not sure why it started working. At that time I saw some explanations about asynchronous execution or a solution to use useEffect() with a dependency but nothing worked reliably. I’d really like to just understand this.

Your code so far

https://codepen.io/pkdvalis/pen/vYbyeWv

Challenge Information:

Front End Development Libraries Projects - Build a 25 + 5 Clock

useEffect(() => {
    setTimer(formatTime(session, 0));
  }, [session]);

  const formatTime = (minutes, seconds) => {
    return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
  };

timer state is updated and reflects the current session value, immediately after any changes to the session state. This keeps the timer synchronized with the session length.

1 Like

I added this to my code, but it no longer renders:

     useEffect(() => {
      setTimer(session);
    }, [session]);

(I’m not formatting the time yet so I omitted that func)

I also added import { useEffect } from "https://esm.sh/react";

1 Like

import React, { useState, useEffect } from “https://esm.sh/react”;
import ReactDOM from “https://esm.sh/react-dom”;

and it will render

1 Like

This worked! Thanks!

But I was already using ReactDOM to render the component to the HTML DIV, why did I need to add import ReactDOM from “https://esm.sh/react-dom”; when I started using useEffect?

Also, it works in codepen but I can’t get it to work in VSCode locally where I am working. It stops rendering when I add useEffect.

I added this:

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

but still does not render.

Any ideas?

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

function App() {

    const [breaker, setBreaker] = React.useState("5");
    const [session, setSession] = React.useState("25");
    const [timer, setTimer] = React.useState("25:00");
    const [timerLabel, setTimerLabel] = React.useState("Session");

    const pressBreak = (event) => {
        if (event.target.id == 'break-increment') {
            if (breaker < 60) setBreaker(parseInt(breaker)+1);
        } else {
            if (breaker > 1) setBreaker(parseInt(breaker)-1);
        }
    }

    const pressSession = (event) => {
        if (event.target.id == 'session-increment') {
            if (session < 60) setSession(parseInt(session)+1);
        } else {
            if (session > 1) setSession(parseInt(session)-1);
        }
    }

    const pressReset = (event) => {
        setBreaker("5");
        setSession("25");
        setTimer("25:00");
    }

    useEffect(() => {
        setTimer(session + ":00");
      }, [session]);

  return (
    <>
  <h1 id="title">fCC 25+5 Clock</h1>
  
  <div id="settings">
        <section id="break-label">
            Break Length
            <div id="breakdisplay">
                <div id="break-length">{breaker}</div>
                <button class="btn btn-light" id="break-increment" onClick={pressBreak}>Up</button>
                <button class="btn btn-light" id="break-decrement" onClick={pressBreak}>Down</button>
            </div>
        </section>
    
        <section id="session-label">
            Session Length
            <div id="sessiondisplay">
                <div id="session-length">{session}</div>
                <button class="btn btn-light" id="session-increment" onClick={pressSession}>Up</button>
                <button class="btn btn-light" id="session-decrement" onClick={pressSession}>Down</button>
            </div>
        </section>
    </div>

    <section id="timer-label">
            {timerLabel}
            <div id="time-left">{timer}</div>
            <button class="btn btn-light" id="start_stop">Play/Pause</button>
            <button class="btn btn-light" id="reset" onClick={pressReset}>Reset</button>
        </section>
  
  </>);
}

ReactDOM.render(<App />, document.getElementById('root'));
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>fCC 25+5 Clock</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script src="https://cdn.freecodecamp.org/testable-projects-fcc/v1/bundle.js"></script>
    <link rel="stylesheet" href="./style.css" />
</head>
<body>
    
    <div id="root"></div>
    <script type="text/babel" src="./index.js"></script>
    
    
</body>
</html>

Use Vite locally.

npm create vite@latest

Use Stackblitz or Codesandbox instead of Codepen.

If I try this at the top, locally, it stops rendering:

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

before I even add the useEffect

Trying this (vite), don’t really know what it’s doing though or how to use this.

Am I crazy or is this a lot just to update a variable?

I’ll try Stackblitz or Codesandbox (although Codepen is working rn)

How do I get the fCC test suite to run at another site? I tried this locally in index.html but it didn’t work:

<script src="https://cdn.freecodecamp.org/testable-projects-fcc/v1/bundle.js"></script>

With Vite follow the prompts to set up a React project and add your code to the App.jsx file or create components as needed.

Stackblitz or Codesandbox are much better than Codepen for React and they are as close to a local developer experience as you can get. I would suggest Stackblitz using the Vite React template.

The test script should work locally. Put it at the bottom of the HTML before the closing </body> tag, not in the head.


Using the React/Babel CDN links locally is not a good idea. They are only there so people can quickly try React and they are from before easy to set up build systems like Vite existed.

The https://esm.sh links act as modules you bring in and I’m not sure how well that plays with scripts marked with text/babel (which is for the JSX transform), the script has to be marked and used as a module for https://esm.sh to work.

1 Like

I tried to port everything over to the Vite environment but it’s only partially working and opened up a new category of troubleshooting. Forego for now, but I’ll check it out later. I think I get it, Vite is setting up a local React server environment instead of trying to use those CDN imports?

The test script should work locally. Put it at the bottom of the HTML before the closing </body> tag, not in the head.

:+1: This was the problem, thanks!

For now, I’m going move forward with the project using this:

setSession(parseInt(session)+1);
setTimer(parseInt(session) + 1 + ":00")

It works and seems bulletproof so far. It’s punk rock.

Thanks for all of the help @zaklina and @lasjorg !

One last thing I don’t understand: Why doesn’t setTimer(session + ":00") immediately update timer and re-render?

Why do we need to use useEffect() ?

My understanding is that React will batch renders so it doesn’t update too often. It will update session but then not render timer until the next render, and that’s why it’s always behind. Is that correct?

The issue is you have two state setters and the second relies on the state value set by the first.

One way to do “computed” or “derived” values in React is to just rely on the render and use a non-state variable. You do not always need a useEffect.

function App() {
  const [count, setCount] = useState(0);
  const [doubleCount, setDoubleCount] = useState(0);

  let doubleCountFixed = count * 2;

  function increment() {
    setCount(count + 1);
    setDoubleCount(count * 2);
  }

  return (
    <>
      <button onClick={increment}>count is {count}</button>
      <h2>Buggy double count</h2>
      {doubleCount}
      <h2>Correct double count</h2>
      {doubleCountFixed}
    </>
  );
}

This works but isn’t the only option.

Edit: here is a Stackblitz with the code if you want to play with it.


I would highly recommend going through the React learning on the new React docs site. It is actually pretty good.

Cool, I will keep this in mind! Don’t always need to use state.

In this case though, if you update doubleCountFixed independently it won’t render the change. Would you need to use useEffect dependency to re-render doubleCountFixed?

  function incrementDoubleCountFixed() {
    doubleCountFixed +=1;
  }

  return (
    <>
      <button onClick={increment}>count is {count}</button>
      <button onClick={incrementDoubleCountFixed}>Add 1 to doubleCountFixed</button>

I also just noticed this warning in the console, locally:

Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot

I wonder if this is part of the problem

ReactDOM.createRoot(document.getElementById('clock')).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
  )

This fixed the Warning

1 Like

createRoot: New method to create a root to render or unmount. Use it instead of ReactDOM.render. New features in React 18 don’t work without it.
React DOM Client
yap…

You don’t need a useEffect but there has to be a state change for the re-render to happen. When the state changes the component runs top to bottom.

With just the single count state variable that state has to change. No state is changing inside your incrementDoubleCountFixed click handler.

Same as before, all three values are driven by the count variable/update.

import { useEffect, useState } from 'react';
import './App.css';

function App() {
  const [count, setCount] = useState(0);

  let doubleCount = count * 2;
  let doubleCountPlusOne = doubleCount + 1;

  function increment() {
    setCount((count) => count + 1);
  }

  return (
    <>
      <button onClick={increment}>count is {count}</button>
      <h2>Double count</h2>
      {doubleCount}
      <h2>Double count plus one</h2>
      {doubleCountPlusOne}
    </>
  );
}

export default App;

Separate handlers, now we need a second state variable so React knows something changed when incrementDoubleCount is called.

function App() {
  const [count, setCount] = useState(0);
  const [doubleCountPlusOne, setDoubleCountPlusOne] = useState(0);

  let doubleCount = count * 2;

  function increment() {
    setCount((count) => count + 1);
  }

  function incrementDoubleCount() {
    setDoubleCountPlusOne(doubleCount + 1);
  }

  return (
    <>
      <button onClick={increment}>count is {count}</button>
      <h2>Double count</h2>
      {doubleCount}
      <h2>Double count plus one</h2>
      <button onClick={incrementDoubleCount}>
        Double count plus one is {doubleCountPlusOne}
      </button>
    </>
  );
}

export default App;

I did sort out my local environment eventually and ended up solving this with useEffect and getting a lot more comfortable with that, thankfully.

I realized the problem wasn’t just re-rendering but in fact the state of the variable was updated asynchronously. I had a test that was like:

console.log(timer) //2
setTimer(timer += 1)
console.log(timer) //2

and it would just log the same number twice in a row.

This was extremely helpful: https://upmostly.com/tutorials/how-to-use-the-setstate-callback-in-react

Here’s something extremely important to know about state in React: updating a React component’s state is asynchronous. It does not happen immediately.

Now why would that be desired? I guess a website is naturally async? As well as this: https://upmostly.com/tutorials/react-hooks-simple-introduction

If you’ve written React class components before, you should be familiar with lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount.

The useEffect Hook is all three of these lifecycle methods combined.

This gave me a much better perspective on what useEffect is. componentDidMount, componentDidUpdate, and componentWillUnmount are all very well named, useEffect is such a bizarre name it gives no insight into what it does! In any case, I got a much better understanding in the end, what more can you ask for.

That said, I did watch Landon Schlangen’s “Build a 25 + 5 clock” video to get some insight on this. He never uses useEffect and never seems to have a problem with asynchronous updates. That video is 3 years old though, and I think useEffect was introduced in 2019 so maybe people weren’t using it yet.