React中基于倒计时子组件状态控制按钮禁用时读取状态失败的问题
解决React父子组件状态同步导致按钮禁用逻辑报错的问题
我来帮你搞定这个问题!你遇到的TypeError: Cannot read property 'seconds' of null报错主要有两个原因:
- 组件首次渲染时,子组件还没完成挂载,
this.child.current还是null,直接访问它的seconds属性自然会报错 - 直接读取子组件内部的
state违反了React的单向数据流设计原则,这不是父子组件通信的正确方式
咱们一步步来修复:
第一步:重构子组件,通过回调传递状态给父组件
修改倒计时子组件,添加一个props回调函数,在倒计时状态变化时主动通知父组件当前的剩余秒数,同时把startTimer从render里移出来(不然每次渲染都会重复启动定时器,导致倒计时混乱):
import React from "react"; import PropTypes from "prop-types"; export default class Counter extends React.Component { constructor(props) { super(props); this.state = { time: {}, seconds: 15 }; this.timer = 0; this.startTimer = this.startTimer.bind(this); this.countDown = this.countDown.bind(this); } // 新增:向父组件传递当前秒数的方法 notifyParent() { this.props.onTimeChange && this.props.onTimeChange(this.state.seconds); } secondsToTime(secs){ let hours = Math.floor(secs / (60 * 60)); let divisor_for_minutes = secs % (60 * 60); let minutes = Math.floor(divisor_for_minutes / 60); let divisor_for_seconds = divisor_for_minutes % 60; let seconds = Math.ceil(divisor_for_seconds); let obj = { "h": hours, "m": minutes, "s": seconds }; return obj; } componentDidMount() { let timeLeftVar = this.secondsToTime(this.state.seconds); this.setState({ time: timeLeftVar }, () => { // 初始化时通知父组件 this.notifyParent(); }); this.startTimer(); // 在这里启动定时器,而不是render里 } startTimer() { if (this.timer === 0 && this.state.seconds > 0) { this.timer = setInterval(this.countDown, 1000); } else if (this.timer === 0 && this.state.seconds === 0){ // 注意:不要直接修改state,要用setState this.setState({ seconds: 15 }, () => { this.timer = setInterval(this.countDown, 1000); this.notifyParent(); }); } } countDown() { let seconds = this.state.seconds - 1; this.setState({ time: this.secondsToTime(seconds), seconds: seconds, }, () => { // 状态更新后通知父组件 this.notifyParent(); if (seconds === 0) { clearInterval(this.timer); this.timer = 0; console.log("counter is 0"); } }); } render() { return( <span className={ this.state.seconds === 0 ? 'timerHidden' : 'timerActive' }> ({this.state.time.s}) </span> ); } } // 定义props类型,确保回调函数是可选的函数类型 Counter.propTypes = { onTimeChange: PropTypes.func };
第二步:修改父组件,通过状态控制按钮禁用
父组件维护自己的状态来控制按钮禁用,通过子组件的回调更新这个状态,同时移除直接访问子组件state的代码:
import Counter from '../Counter/Counter.js'; export default class Verify extends React.Component { constructor(props) { super(props); this.child = React.createRef(); // 新增:维护按钮禁用状态,初始值根据倒计时初始秒数设置 this.state = { username: this.username, email: this.email, code: "", isButtonDisabled: true // 初始倒计时是15秒,所以按钮默认禁用 }; } resetTimer = () => { this.child.current.startTimer(); }; resendConfirmationCode = async e =>{ this.resetTimer(); // ... 你的其他逻辑 } // 新增:接收子组件传来的秒数,更新按钮禁用状态 handleTimeChange = (seconds) => { this.setState({ isButtonDisabled: seconds > 0 }); } render() { return ( <button className="btn btn-primary register empty" type="button" disabled={this.state.isButtonDisabled} onClick={this.resendConfirmationCode} > Resend code <Counter ref={this.child} onTimeChange={this.handleTimeChange}/> </button> ); } }
额外注意点
- 永远不要直接修改子组件的
state(比如你之前写的this.state.seconds = 15),必须用setState来更新,否则React无法触发重新渲染 - 定时器要记得在组件卸载时清理,避免内存泄漏,可以在子组件的
componentWillUnmount里添加clearInterval(this.timer)
内容的提问来源于stack exchange,提问作者Eric Mitjans
相关产品推荐
相关产品推荐

