1

Happy 2k22! I am building a countdown timer. I have two files. The first file takes the countDown time from input and the second from select dropdown .

I have implemented an increment button in the first file. It increases the countDown time by inc seconds i.e. time = time + inc.

So what's peculiar?

Thing is that when inc is replaced with any constant value, it works properly.

<button onClick={() => setSecondsRemaining(secondsRemaining + 3)}>
      Increment {inc} sec
</button>

But when I used the input to enter the value and supply it through the variable inc, then it does not work. It increases randomly.

<button onClick={() => setSecondsRemaining(secondsRemaining + inc)}>
      Increment {inc} sec
</button>

You can visit InputCountDown.js here

This is the full code:

import React, { useState, useEffect, useRef } from "react";
import "./styles.css";

const STATUS = {
  STARTED: "Started",
  STOPPED: "Stopped"
};

export default function InputCountDown() {
  const [time, setTime] = useState(0);
  const [inc, setInc] = useState(0);

  const handleOnChange = (e) => {
    //console.log(e.target.value);
    setTime(e.target.value);
  };

  const handleOnChangeIncrement = (e) => {
    console.log(e.target.value);
    setInc(e.target.value);
  };

  const [secondsRemaining, setSecondsRemaining] = useState(time * 60);

  //console.log(time);

  const [status, setStatus] = useState(STATUS.STOPPED);

  const secondsToDisplay = secondsRemaining % 60;
  const minutesRemaining = (secondsRemaining - secondsToDisplay) / 60;
  const minutesToDisplay = minutesRemaining % 60;
  const hoursToDisplay = (minutesRemaining - minutesToDisplay) / 60;

  const handleStart = () => {
    setStatus(STATUS.STARTED);
    setSecondsRemaining(time * 60);
  };
  const handleStop = () => {
    setStatus(STATUS.STOPPED);
  };
  const handleReset = () => {
    setStatus(STATUS.STOPPED);
    setSecondsRemaining(time * 60);
  };
  useInterval(
    () => {
      if (secondsRemaining > 0) {
        setSecondsRemaining(secondsRemaining - 1);
      } else {
        setStatus(STATUS.STOPPED);
      }
    },
    status === STATUS.STARTED ? 1000 : null
    // passing null stops the interval
  );
  return (
    <>
      <div className="App">
        <h1>Countdown Using Input</h1>
        <div style={{ padding: "12px" }}>
          <label htmlFor="time"> Enter time in minutes </label>
          <input
            type="text"
            id="time"
            name="time"
            value={time}
            onChange={(e) => handleOnChange(e)}
          />
        </div>
        <div style={{ padding: "12px" }}>
          <label htmlFor="inc"> Enter increment </label>
          <input
            type="text"
            id="inc"
            name="inc"
            value={inc}
            onChange={(e) => handleOnChangeIncrement(e)}
          />
        </div>

        <button onClick={handleStart} type="button">
          Start
        </button>
        <button onClick={handleStop} type="button">
          Stop
        </button>
        <button onClick={handleReset} type="button">
          Reset
        </button>
        <div style={{ padding: 20, fontSize: "40px" }}>
          {twoDigits(hoursToDisplay)}:{twoDigits(minutesToDisplay)}:
          {twoDigits(secondsToDisplay)}
          <div>
            <button onClick={() => setSecondsRemaining(secondsRemaining + inc)}>
              Increment {inc} sec
            </button>
          </div>
        </div>
        <div>Status: {status}</div>
      </div>
    </>
  );
}

// source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
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]);
}

// https://stackoverflow.com/a/2998874/1673761
const twoDigits = (num) => String(num).padStart(2, "0");

You can visit InputCountDown.js here

Please help me.

MagnusEffect
  • 3,363
  • 1
  • 16
  • 41

2 Answers2

2

It's a very basic rule you missed mate. Whenever you get anything from TextInput, its always a string, even if you entered an integer. Try this:

<button onClick={() => setSecondsRemaining(secondsRemaining + parseInt(inc))}>
      Increment {inc} sec
</button>
Akshay Shenoy
  • 1,194
  • 8
  • 10
  • 1
    Yeah, and `300 + "5"` does not result in `305` but in `"3005"`. Alternatively to `parseInt` you can also just use the [unary `+` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Unary_plus) `secondsRemaining + +inc` – derpirscher Jan 03 '22 at 16:48
1

When consuming values from input elements recall that these are always string type values. Best to maintain the number type state invariant, so do the conversion from string to number when updating state.

const handleOnChange = (e) => {
  setTime(Number(e.target.value));
};

const handleOnChangeIncrement = (e) => {
  setInc(Number(e.target.value));
};

To help ensure users are entering number-like data into the inputs, specify each input to be type="number".

<div style={{ padding: "12px" }}>
  <label htmlFor="time"> Enter time in minutes </label>
  <input
    type="number" // <-- number type
    id="time"
    name="time"
    value={time}
    onChange={handleOnChange}
  />
</div>
<div style={{ padding: "12px" }}>
  <label htmlFor="inc"> Enter increment </label>
  <input
    type="number" // <-- number type
    id="inc"
    name="inc"
    value={inc}
    onChange={handleOnChangeIncrement}
  />
</div>

Recall also that when enqueueing state updates that if the next state value depends on the previous state value, i.e. when incrementing counts, etc..., that you should use a functional state update.

<button onClick={() => setSecondsRemaining(time => time + inc)}>
  Increment {inc} sec
</button>

Edit unusual-behavior-when-value-of-variable-change-easy

Drew Reese
  • 165,259
  • 14
  • 153
  • 181