Build a One-Time Password Generator - Build a One-Time Password Generator

Tell us what’s happening:

It’s passing everything but 12 and 13. It counts down from 5 to 2 and then jumps to the expiration message, skipping 1 and 0, the first time I run it. And then in subsequent runs, it starts at 0, then goes to 5 and counts down to 2 again. I can’t figure out what I’m doing wrong.

Your code so far

<!-- file: index.html -->
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <title>OTP Generator</title>
    <link rel="stylesheet" href="styles.css" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
    <script
      data-plugins="transform-modules-umd"
      type="text/babel"
      src="index.jsx"
    ></script>
</head>

<body>
    <div id="root"></div>
    <script
      data-plugins="transform-modules-umd"
      type="text/babel"
      data-presets="react"
      data-type="module"
    >
      import { OTPGenerator } from './index.jsx';
      ReactDOM.createRoot(document.getElementById('root')).render(<OTPGenerator />);
    </script>
</body>

</html>
/* file: styles.css */

/* file: index.jsx */
const { useState, useEffect, useRef } = React;

const genCode = () => {
  const intPool = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
  let genedCode = '';
  for (let i = 1; i < 7; i++) {
      genedCode = genedCode + intPool[Math.floor(Math.random() * intPool.length)].toString()
  }
  return genedCode
};



export const OTPGenerator = () => {
  const [code, setCode] = useState("Click 'Generate OTP' to get a code")
  const [timerBox, setTimerBox] = useState('')
  const [timerCount, setTimerCount] = useState(5)
  const [isDisabled, setIsDisabled] = useState(false)
  
  useEffect(() => {
    if (!isDisabled) return;
    if (timerCount === 0) {
      setTimerBox("OTP expired. Click the button to generate a new OTP.");
      setIsDisabled(false)
    }
    const counter = setInterval(() => {
      setTimerCount(timerCount - 1);
      setTimerBox(`Expires in: ${timerCount} seconds`);
      console.log(timerCount);  
    }, 1000)
    return () => clearInterval(counter)
  }, [timerCount, isDisabled]) 

  
  const handleClick = () => {
    setCode(genCode);
    setTimerCount(5);
    setTimerBox(`Expires in: ${timerCount} seconds`);
    setIsDisabled(true);
  }

  return(
    <div class="container">
      <h1 id="otp-title">OTP Generator</h1>
      <h2 id="otp-display">{code}</h2>
      <p id="otp-timer" aria-live="polite">{timerBox}</p>

      <button id="generate-otp-button" onClick={handleClick}disabled={isDisabled}>Generate OTP</button>
    </div>
  )
};

Your browser information:

User Agent is: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36

Challenge Information:

Build a One-Time Password Generator - Build a One-Time Password Generator

GitHub Link: freeCodeCamp/curriculum/challenges/english/blocks/lab-one-time-password-generator/67c562286b29447da020d407.md at main · freeCodeCamp/freeCodeCamp · GitHub

The thing is state updates are not instant. Ie. for

      setTimerCount(timerCount - 1);
      setTimerBox(`Expires in: ${timerCount} seconds`);
      console.log(timerCount);

It’s not guaranteed that after new timerCount is set, the next line will already have updated value (it’s almost guaranteed to be the opposite).

What might be even more surprising, when calling setTimerCount(timerCount - 1), it’s not guaranteed, the timerCount will be the latest value. See useState – React

Per the linked article, I changed it to

setTimerCount(prev => prev -1);

but it is still doing the same thing.

Reading further down, into the troubleshooting section, I read:

"This is because states behaves like a snapshot. Updating state requests another render with the new state value, but does not affect the count JavaScript variable in your already-running event handler.

If you need to use the next state, you can save it in a variable before passing it to the set function"

So I tried doing it by setting a new variable to timerCount +1 and then setting timerCount to the new variable, and that also had no effect.

I feel like I’ve totally missed the point of whatever it is you’re trying to tell me.

Could you share updated code?

I didn’t look at it over the weekend (I mostly do these courses in my downtime at my job, and I was off) so I don’t super remember where I was at. I think I just changed everything to use the “prev => newvalue” format, and it didn’t make a difference. Current code, as I left it on Friday:

const { useState, useEffect, useRef } = React;

const genCode = () => {
  const intPool = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
  let genedCode = '';
  for (let i = 1; i < 7; i++) {
      genedCode = genedCode + intPool[Math.floor(Math.random() * intPool.length)].toString()
  }
  return genedCode
};



export const OTPGenerator = () => {
  const [code, setCode] = useState("Click 'Generate OTP' to get a code")
  const [timerBox, setTimerBox] = useState('')
  const [timerCount, setTimerCount] = useState(5)
  const [isDisabled, setIsDisabled] = useState(false)
  
  useEffect(() => {
    if (!isDisabled) return;
    if (timerCount === 0) {
      setTimerBox("OTP expired. Click the button to generate a new OTP.");
      setIsDisabled(prev => false)
    }
    const counter = setInterval(() => {
      const newCount = timerCount - 1
      setTimerCount(prev => newCount);
      setTimerBox(prev => `Expires in: ${timerCount} seconds`);
      console.log(newCount);  
    }, 1000)
    return () => clearInterval(counter)
  }, [timerCount, isDisabled]) 

  
  const handleClick = () => {
    setCode(prev => genCode());
    setTimerCount(prev => 5);
    setTimerBox(prev => `Expires in: ${timerCount} seconds`);
    setIsDisabled(prev => true);
  }

  return(
    <div class="container">
      <h1 id="otp-title">OTP Generator</h1>
      <h2 id="otp-display">{code}</h2>
      <p id="otp-timer" aria-live="polite">{timerBox}</p>

      <button id="generate-otp-button" onClick={handleClick}disabled={isDisabled}>Generate OTP</button>
    </div>
  )
};

If the previous value is not used in the setter function, there’s no point really in creating updating function.

Ie this is how it could be used:

setTimerCount(prev => prev - 1);

Otherwise, take a look at two places:

    const counter = setInterval(() => {
      const newCount = timerCount - 1
      setTimerCount(prev => newCount);
      setTimerBox(prev => `Expires in: ${timerCount} seconds`);
      console.log(newCount);  
    }, 1000)
    return () => clearInterval(counter)
  const handleClick = () => {
    setCode(prev => genCode());
    setTimerCount(prev => 5);
    setTimerBox(prev => `Expires in: ${timerCount} seconds`);
    setIsDisabled(prev => true);
  }

What values have timerCount, when expire message is being set with setTimerBox? Is it the same that was just set with setTimerCount?

That’s what it’s supposed to be, yes. I’m trying to first update the value of timerCount and then update the message displayed by timerBox to reflect that. But it’s not behaving that way. It’s updating the displayed message immediately but not updating the value of timerCount immediately. I think.

Yes, the timerCount is not yet updated, when new message is set. So how it can be ensured setTimerCount will use correct value?

I don’t know. useEffect is set to refresh whenever timerCount or isDisabled are changed, and setInterval is set to subtract 1 from timerCount every 1000 ms, per my understanding of my code, so I clearly don’t understand what my code is doing. When I log timerCount to console, I can see that it is being properly modified every second, but it is not being reflected in the message countdown.

Take a look how this will get logged to console:

    const counter = setInterval(() => {
      const newCount = timerCount - 1
      setTimerCount(prev => newCount);
      setTimerBox(prev => { 
        console.log('newCount', newCount);
        console.log('timerCount "in" setTimerBox', timerCount);
        return `Expires in: ${timerCount} seconds`;
      })
      console.log(newCount);
    }, 1000)

timerCount might, or might not be updated, to be the right value when setTimerBox is callled. However the right value is not far off.

I moved the setTimerBox command to outside of the setInterval block, and that fixed the countdown not going to 0, and starting lagged, but it broke setting the expiry message. So I put the setTimerBox in an if-block so that it wouldn’t update it if it were not above 0, and that made the whole thing work, as below:

  useEffect(() => {
    if (!isDisabled) {
      setTimerCount(5);
      return;
      }
    if (timerCount == 0) {
      setTimerBox("OTP expired. Click the button to generate a new OTP.");
      setIsDisabled(prev => false);
    }
    if (timerCount > 0) {
      setTimerBox(prev => `Expires in: ${timerCount} seconds`);
    }
    const counter = setInterval(() => {
      const newCount = timerCount - 1
      setTimerCount(prev => newCount);

    }, 1000)
      
    return () => clearInterval(counter);
  }, [timerCount, isDisabled])

I don’t think I really understand why, tho. Thank you for your time and patience!