1

I have a MyComponent that renders a Timer component. My current setup is like this:

MyComponent.render:

render () {
  return <Timer time={this.state.time} lag={this.lag || 0} />
}

Timer:

class Timer extends Component {
  constructor(props) {
    super(props);
    this.state = {
      time: this.props.time,
    };
  }

  startTimer = (duration) => {
    if (duration > 0){
      this.on = true;
      let timer = duration * 1000 + this.props.lag;
      var s = setInterval(() => {
        this.setState({time: Math.round(timer/1000)});
        timer = timer - 500;
          if (timer <= 0) {
            this.on = false;
            clearInterval(s);
          }
      }, 500);
    }
  }

  componentDidMount = () => {
    this.startTimer(this.props.time);
  }

  render() {
    return (
      <div className="Timer-container">
        <div className="Timer-value">{this.state.time}</div>
      </div>
    );
  }
}

As you can see, when the Timer is initialized, it immediately starts counting down. On subsequent renders of MyComponent, I want to restart the Timer, even if the time prop doesn't change. In other words, I want it to "reinitialize" on every render. How do I achieve this?

actinidia
  • 236
  • 3
  • 17
  • Have you tried calling function from within `render()`? – Justinas Jan 25 '21 at 07:14
  • @Justinas I'm not sure what you mean; could you please elaborate? – actinidia Jan 25 '21 at 07:17
  • Inside render: `clearInterval(this.s); this.startTimer(1000)` – Justinas Jan 25 '21 at 07:20
  • 1
    don't think you want put into `render()`, because it will call the function everytime when local state change, I think you can call the function in `componentWillReceiveProps`, it will skip local state, but only listen on parent re-render – Yi Zhou Jan 25 '21 at 07:20
  • you may need to use `forceUpdate` to force component re-render again https://reactjs.org/docs/react-component.html#forceupdate – Thanh Jan 25 '21 at 07:21
  • @Justinas This seems to make the timer stuck at `duration`; when the `setState` call in the function calls `render`, the countdown gets restarted. – actinidia Jan 25 '21 at 07:44
  • 1
    As a workaround you can wrap `time` into an object, each time you want to re-start the timer you pass a new object even with the same time `time={value:500}` and in the component read is as `props.time.value`. You might need to refactor your code so you can stop the old timer (if you want to stop it of course) – Nadia Chibrikova Jan 25 '21 at 10:32
  • @Yi-Zhou Note that [componentwillreceiveprops is deprecated](https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops) – kca Jan 26 '21 at 19:22
  • @kca yea, but assume this is older version as still using lifecycle instead hooks – Yi Zhou Jan 26 '21 at 19:28

1 Answers1

1
  1. First of all, to reset the counter, you need to store something in the state,
  • either the interval (so you can clear it)
  • or the current time (so you can set it to the initial value).
  1. As you want to do something if the parent re-rendered (but the props didn't change), basically what you need to check is why your component updated. An answer to that would be "Trace why a React component is re-rendering"

A quick way for your example would be to check if the state has changed (not recommended):

componentDidUpdate(prevProps, prevState, snapshot) {
    if( prevState === this.state ){
        clearInterval( this.state.interval );
        this.startTimer( this.props.time );
    }
}

Another quick solution would be (if it is an option for you) to pass a shouldRerender property to the component, and then check for this property inside the component:

// -- inside MyComponent
render () {
  return <Timer
    time={ this.state.time }
    lag={ this.lag || 0 }
    shouldRerender={ {/* just an empty object */} } />;
}

// -- inside Timer
componentDidUpdate(prevProps, prevState, snapshot) {
    if( prevProps.shouldRerender !== this.props.shouldRerender ){
        clearInterval( this.state.interval );
        this.startTimer( this.props.time );
    }
}

That looks a bit "dirty" to me. A cleaner way would be to pass some state to shouldRerender, which changes on every update (e.g. just an increasing number).

However, I think the approach to check if parent rendered is not the React way. I, personally, do consider if a component renders or not an implementation detail (I don't know if that's correct to say), that is, I don't care when React decides to render, I only care for props and state (basically).

I would recommend to think about what actually is "cause and effect", what is the reason why you want to reset the timer. Probably the re-render of the parent is only the effect of some other cause, which you might be able to use for your time reset, too.

Here some different concepts that might be useful for use cases I can imagine:

  • not use one Time instance, but destroy and create inside parent when needed, maybe also using a key prop.
  • use a HOC (like withTimer) or custom hook (like useTimer), injecting a reset() function (plus create a separate TimerView component)
  • keep the time state in MyComponent, passing time and onChange down to the Timer component (<Timer time={ this.state.time } onChange={ time => { this.setState({ time: time }); } } />), then both MyComponent and Timer can set / reset the time.
kca
  • 4,856
  • 1
  • 20
  • 41