reactjs 当输入更改时,意外的React倒计时组件重新呈现

wwwo4jvm  于 2023-10-17  发布在  React
关注(0)|答案(2)|浏览(106)

我已经创建了一个自定义的倒计时组件使用react-countdown包。一般来说,它工作得很好。但是当我在页面中输入文本时,它会以某种方式再次呈现并重置为初始时间。我已经检查了输入的onChange事件,但它与倒计时无关。我真的很困惑为什么会这样。
我的想法是在创造倒计时是,如果我改变了关键 prop 的倒计时组件,我将有一个新的倒计时。因为我知道如果我们改变了react组件中的关键 prop ,它们会重新渲染。
倒计时组件:

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

r6hnlfcb1#

我发现问题了。这是真正产生问题的部分:

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

yrdbyhpb2#

成功了!
基本上,在重新呈现时,Date.now()将捕获一个新的时间(重新呈现的时间),而不是“记住”倒计时开始的初始时间。
通过将其锁定在useRef中,即使在重新渲染之后,ref中的值也不会更改。

相关问题