React – method run right times via state but run double times when Parent Component changing state

I am trying to build a page with some data initialized at first time mounted, and update when websocket server give a response msg when certain button click event is triggered, also I need to ban the button aka. disabled, and tell the user in how many seconds the button is clickable again.

My first thought is, single component, update via states, give a state to the counter, then use setTimeout to count down 1 every 1000ms, turned out that the counter “banCount” worked well, until I add the websocket.send(), then it counted down 2 every time.

I thought that would be because when the websocket server responsed, the state is change, so the whole component is updated, the counter is messed up.

So, I had an idea, separating it into a child component, with its own state, but do nothing when in the life cycle of componentWillReceiveProps, and it will not receive props, so it will just work with it is own state. But the result is with or without separating the counter into a child component, they worked the same.

parent component:

import React from 'react';
import ReactDOM from 'react-dom';

import TestChild from './testChild/testChild';

class TestParent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
     wsData: null,
   };
 }

 componentWillMount() {
   this.wsClient = new WebSocket("ws://localhost:9000/server", 'echo-protocol');
   this.wsClient.onmessage = msg => {
     if (msg) {
       this.setState({
         wsData: msg.data
       });
     }
   };
 }

 render() {
   const data = () => {
     if (this.state.wsData) {
       return this.state.wsData;
     } else {
       return "waiting data";
     }
   };
   return (
     <div>
       <div>{data()}</div>
       <TestChild wsClient={this.wsClient}/>
     </div>
   );
 }    
}

ReactDOM.render(
   <TestParent />,
   document.getElementById('reactWrapper')
);

and the Child Component:

import React from 'react';

class TestChild extends React.Component {
  constructor(props) {
    super(props);
    this.count = null;
    this.state = {
      banCount: this.count
    };
    this.wsClient = this.props.wsClient;
    this.countupdate = 0;
  }

  banCount() {
    this.setState({
      banCount: this.count
    });
  }

  callNext(n) {
    this.wsClient.send('can you hear me');
    this.count = n;
    this.banCount();
  }

  componentDidUpdate() {
    if (this.count > 0) {
      setTimeout(() => {
        this.count -= 1;
        this.banCount();
      }, 1000);
    } else if (this.count === 0) {
      this.count = null;
      this.banCount();
    }
  }

  render() {
    return <button onClick={() => this.callNext(3)}>click me {this.state.banCount}</button>;
  }    
}

export default TestChild;

Please ignore ‘whether the server and websocket connection’ works part, they are fine.

I don’t know why, I even had not updated Child component, I am really new to React, I really do not know how to debug this, I read this code for hours, but it is just too complicated for me.

Why it counted down 2 every time? and for sure I am wrong, what is the right way.

Please help me with only React and vanilla Javascript, I had not use Redux or Flux and even did not know what they are, thank you.

This is NOT tested code, but should help you to build what you want, I didn’t tested your component but I suspect that your setTimeout() is called several times.

import React from 'react';

class TestChild extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: null,
    };
  }

  startCountDown() {
    var newCount = this.state.count -1;
    if(newCount === 0){
       clearTimeout(this.timer);
    }
    this.setState({
      count: newCount,
    });

  }

  callNext(n) {
    this.wsClient.send('can you hear me');
    this.setState({
      count: n,
    });
    this.timer = setTimeout(() => {
      startCountDown();
    }, 1000);
  }
  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  render() {
    return <button disabled={this.state.count>0} onClick={() => 
       this.callNext(3)}>click me {this.state.count}</button>;
  }    
}

export default TestChild;

Finally I worked it out.

It is because React will re-render all the child component with or without setting children’s new states. The only way to stop it from re-render is to use ShouldComponentUpdate, so:

shouldComponentUpdate() {
  return this.state.banCount !== null;
}

will work, as when the child component receiving props after websocket.send(), this.count is still null, but right after the websocket.send(), this.count is set to 3, so the child component will update since.

Also another workround:

callNext(n) {
  this.wsClient.send('can you hear me');
  this.count = n;
}

componentWillReceiveProps(nextProps) {
  this.data = nextProps.datas;
  this.setState({
    banCount: this.count
  });
}

in this workround, without shouldComponentUpdate() the child component will always re-render when its parent receive websocket data, so in the click handler function, stop calling bancount(), so it would not update itself, but set the state when receive nextProps, that will trigger the re-render.

To sum all above:

child component will always re-render with or without setting state via new props unless shouldComponentUpdate return false, I alreay called bancount() in the click handler function, trigger child component to update the state itself, but after the parent component receiving websocket data, it triggered state updating again, that is why it run double times.