reactjscountdown

Unexpected React countdown component re-render, when input changes


I have created a custom countdown component using react-countdown package. It work fines in general. But when I type inside a text input in my page, it will render again somehow and resets to its initial time. I have checked the onChange event of the input but it has nothing to do with the countdown. I'm really confused why this happens.

My idea in creating the countdown was that, if I change the key prop of countdown component, I will have a fresh countdown. Because as I know if we change the key prop in react components they will re-render.

Countdown component:

const AgapeCountdown = ({ duration, children, restartKey, ...props }) => {
  const classes = useStyles();
  const defaultRenderer = ({ hours, minutes, seconds, completed }) => {
    if (completed) {
      return children;
    }

    return (
      <span className={classes.root}>
        {minutes}:{seconds}
      </span>
    );
  };
  return (
    <Countdown
      renderer={defaultRenderer}
      date={Date.now() + duration}
      key={restartKey}
      {...props}
    />
  );
};

Usage:

<AgapeCountdown duration={10000} restartKey={countdownKey}>
  <AgapeButton onClick={handleResendOtpClick} className={classes.textButton}>
    ارسال مجدد کد
  </AgapeButton>
</AgapeCountdown>;

input element in the same page:

<AgapeTextField
            placeholder="مثال: ۱۲۳۴۵"
            variant="outlined"
            fullWidth
            onChange={handleOtpChange}
            value={otp}
            helperText={otpHelperText}
            error={otpHelperText}
          />

input change handler:

const handleOtpChange = (event) => {
  if (otpRegex.test(event.target.value)) {
    setOtpHelperText(null);
    setDisableOtpAction(false);
    setOtp(event.target.value).then(() => {
      nextButtonClicked();
    });
  } else {
    setOtp(event.target.value);
    setOtpHelperText(helperInvalidOtp);
    setDisableOtpAction(true);
  }
};

where countdownKey get updated:

const handleResendOtpClick = () => {
  setCountdownKey(countdownKey + 1);
  console.log('hello from resendotpclick');
  registerApiService({
    mobile: phoneNumberPure,
  })
    .then((response) => {
      if (response.status === 200) {
        // TODO show user that otp code resent.
      }
    })
    .catch((error) => {
      // TODO show user that otp code resend failed.
    });
};

full code for deeper inspection:

const LoginStep2 = ({ dialogHandler, ...props }) => {
  const classes = useStyles(props);
  const setIsLoginOpen = dialogHandler;
  const dispatch = useDispatch();
  const phoneNumberPure = useSelector(selectPhone);
  const ELogin = useSelector(selectELogin);
  const [otp, setOtp] = useStateWithPromise(null);
  const [otpHelperText, setOtpHelperText] = React.useState(null);
  const [disableOtpAction, setDisableOtpAction] = React.useState(true);
  const [phoneNumber, setPhoneNumber] = React.useState('');
  const [countdownKey, setCountdownKey] = React.useState(1);

  React.useEffect(() => {
    if (phoneNumberPure) {
      setPhoneNumber(phoneNumberPure.split('-')[1]);
    }
  }, [phoneNumberPure]);

  const handlePrevIconClicked = () => {
    if (ELogin) {
      dispatch(next());
      return;
    }
    dispatch(prev());
  };

  const nextButtonClicked = () => {
    setDisableOtpAction(true);
    const convertedOtp = convertPersianDigitsToEnglish(otp);
    loginApiService({ mobile: phoneNumberPure, otp: convertedOtp })
      .then((response) => {
        if (response.status === 200) {
          if (response.data.access_token) {
            const jsonUser = {
              phone: phoneNumberPure,
              token: response.data.access_token,
              social: null,
              email: null,
            };
            localStorage.setItem('user', JSON.stringify(jsonUser));
            if (ELogin) {
              setIsLoginOpen(false);
              return;
            }
            dispatch(next());
          }
        } else if (response.status === 404) {
          setOtpHelperText(helperWrongOtp);
        }
      })
      .catch((error) => {
        setOtpHelperText(helperWrongOtp);
      })
      .finally(() => {
        setTimeout(() => {
          setDisableOtpAction(false);
        }, 1000);
      });
  };

  const handleResendOtpClick = () => {
    setCountdownKey(countdownKey + 1);
    console.log('hello from resendotpclick');
    registerApiService({
      mobile: phoneNumberPure,
    })
      .then((response) => {
        if (response.status === 200) {
          // TODO show user that otp code resent.
        }
      })
      .catch((error) => {
        // TODO show user that otp code resend failed.
      });
  };

  const handleOtpChange = (event) => {
    if (otpRegex.test(event.target.value)) {
      setOtpHelperText(null);
      setDisableOtpAction(false);
      setOtp(event.target.value).then(() => {
        nextButtonClicked();
      });
    } else {
      setOtp(event.target.value);
      setOtpHelperText(helperInvalidOtp);
      setDisableOtpAction(true);
    }
  };
  return (
    <Grid container>
      <Grid item xs={12}>
        <IconButton onClick={handlePrevIconClicked}>
          <BsArrowRight className={classes.arrowIcon} />
        </IconButton>
      </Grid>
      <Grid
        item
        container
        xs={12}
        justify="center"
        className={classes.logoContainer}
      >
        <img src={AgapeLogo} alt="لوگوی آگاپه" />
      </Grid>
      <Grid
        item
        container
        xs={12}
        justify="center"
        className={classes.loginTitle}
      >
        <Typography variant="h4">کد تایید را وارد نمایید</Typography>
      </Grid>
      <Grid item xs={12}>
        <Typography variant="body1" className={classes.noMargin}>
          کد تایید به شماره
          <span className={classes.phoneNumberContainer}>{phoneNumber}</span>
          ارسال گردید
        </Typography>
      </Grid>
      <Grid
        item
        container
        xs={12}
        justify="space-between"
        className={classes.loginInputs}
      >
        <Grid item xs={12}>
          <AgapeTextField
            placeholder="مثال: ۱۲۳۴۵"
            variant="outlined"
            fullWidth
            onChange={handleOtpChange}
            value={otp}
            helperText={otpHelperText}
            error={otpHelperText}
          />
        </Grid>
      </Grid>
      <Grid item xs={12}>
        <AgapeButton
          color="primary"
          disabled={disableOtpAction}
          onClick={nextButtonClicked}
          fullWidth
        >
          تایید
        </AgapeButton>
      </Grid>

      <Grid
        item
        container
        xs={12}
        justify="space-between"
        className={classes.textButtonsContainer}
      >
        <Grid item xs={4}>
          <AgapeCountdown duration={10000} restartKey={countdownKey}>
            <AgapeButton
              onClick={handleResendOtpClick}
              className={classes.textButton}
            >
              ارسال مجدد کد
            </AgapeButton>
          </AgapeCountdown>
        </Grid>
        <Grid item xs={4} className={classes.callButton}>
          <AgapeButton className={classes.textButton}>
            دریافت از طریق تماس
          </AgapeButton>
        </Grid>
      </Grid>
    </Grid>
  );
};

Solution

  • I found the problem. this is the part actually creates the problem:

    <Countdown
          renderer={defaultRenderer}
          date={Date.now() + duration}
          key={restartKey}
          {...props}
        />
    

    the Date.now() will update. And it makes the countdown to restart. for solving this problem I used a ref which stop the component to re-render if it changes:

    const AgapeCountdown = ({ duration, children, restartKey, ...props }) => {
      const classes = useStyles();
      const startDate = React.useRef(Date.now());
      const defaultRenderer = ({ hours, minutes, seconds, completed }) => {
        return (
          <span className={classes.root}>
            {minutes}:{seconds}
          </span>
        );
      };
      return (
        <Countdown
          renderer={defaultRenderer}
          date={startDate.current + duration}
          key={restartKey}
          {...props}
        />
      );
    };