我使用 react-countdown 包创建了一个自定义倒计时组件。 一般来说,它工作得很好。但是,当我在页面中的文本输入中键入内容时,它会以某种方式再次呈现并重置为初始时间。我已经检查了输入的 onChange 事件,但它与倒计时无关。我真的很困惑为什么会发生这种情况。
我创建倒计时的想法是,如果我更改倒计时组件的 key 属性,我将有一个新的倒计时。因为据我所知,如果我们更改反应组件中的关键道具,它们将重新渲染。
倒计时组件:
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}
/>
);
};
用途:
<AgapeCountdown duration={10000} restartKey={countdownKey}>
<AgapeButton onClick={handleResendOtpClick} className={classes.textButton}>
ارسال مجدد کد
</AgapeButton>
</AgapeCountdown>;
同一页面中的输入元素:
<AgapeTextField
placeholder="مثال: ۱۲۳۴۵"
variant="outlined"
fullWidth
onChange={handleOtpChange}
value={otp}
helperText={otpHelperText}
error={otpHelperText}
/>
输入更改处理程序:
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);
}
};
countdownKey 更新的位置:
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 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>
);
};
我发现问题了。这实际上是造成问题的部分:
<Countdown
renderer={defaultRenderer}
date={Date.now() + duration}
key={restartKey}
{...props}
/>
Date.now()
将会更新。并且它使重新启动倒计时。为了解决这个问题,我使用了 ref
来阻止组件在发生变化时重新渲染:
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}
/>
);
};
这有效!
基本上,在重新渲染时,Date.now() 将捕获新时间(重新渲染的时间),而不是“记住”倒计时开始的初始时间。
通过将其锁定在 useRef 中,即使在重新渲染之后,ref 中的值也不会改变。