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>
);
};
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}
/>
);
};