Search code examples
reactjsreact-component

Disabling button based on child component state in React


I am using a countdown component as a child component. I want to disable/reable a button based on the state value of the counter, but I can't seem to read the value correctly.

This is what I have tried. This is the countdown component:

import React from "react";
import PropTypes from "prop-types";


export default class Counter extends React.Component {
  constructor() {
    super();
    this.state = { time: {}, seconds: 15 };
    this.timer = 0;
    this.startTimer = this.startTimer.bind(this);
    this.countDown = this.countDown.bind(this);
  }

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

  startTimer() {
    if (this.timer === 0 && this.state.seconds > 0) {
      this.timer = setInterval(this.countDown, 1000);
    } else if ((this.timer === 0 && this.state.seconds === 0)){
      this.state.seconds = 15;
      this.timer = setInterval(this.countDown, 1000);
    }
  }

  countDown() {
    // Remove one second, set state so a re-render happens.
    let seconds = this.state.seconds - 1;
    this.setState({
      time: this.secondsToTime(seconds),
      seconds: seconds,
    });
    
    // Check if we're at zero.
    if (seconds === 0) { 
      clearInterval(this.timer);
      this.timer = 0;
      console.log("counter is 0");
      console.log(this.state.seconds);
      console.log(this.timer);
    }
  }



  render() {
    this.startTimer();
    return(
      <span className={
        this.state.seconds === 0 ? 'timerHidden' : 'timerActive'
      }>
        ({this.state.time.s})
      </span>
    );
  }
}

And how I read it and reset it in the parent component:

import Counter from '../Counter/Counter.js';

export default class Verify extends React.Component {
  state = {
    username: this.username,
    email: this.email,
    code: ""
};
  
constructor(props) {
    super(props);
    this.child = React.createRef();
}

resetTimer = () => {
    this.child.current.startTimer();
};

resendConfirmationCode = async e =>{

    this.resetTimer();
    ...
}

return (

     <button 
         className="btn btn-primary register empty" 
         type="button"
         disabled={this.child.current.seconds > 0} 
         onClick={this.resendConfirmationCode}>Resend code <Counter ref={this.child}/>
     </button>
             
    
);

Inserting the counter works fine, reseting also, but the disabling of the button throws the following error:

TypeError: Cannot read property 'seconds' of null
Verify.render
> 109 |     disabled={this.child.current.seconds > 0} 

Solution

  • The this.child ref will be null/undefined on the initial render. Since you probably also want to disable the button if the counter component isn't available for some reason, you can just check if the ref's current value is falsey or if it is truthy and state.seconds of the child greater than 0.

    <button 
      ...
      disabled={!this.child.current || this.child.current.state.seconds > 0} 
      onClick={this.resendConfirmationCode}
    >
      Resend code 
    </button>
    <Counter ref={this.child} />
    

    If we invert the second condition we can combine them into a single comparison using Optional Chaining.

    <button 
      ...
      disabled={!this.child.current?.state.seconds <= 0} 
      onClick={this.resendConfirmationCode}
    >
      Resend code 
    </button>
    <Counter ref={this.child} />