Milo K
Milo K

Reputation: 65

ReactJS: How to refresh component without 1 click delay

I'm learning React and I have problem with refreshing a child component when parent's state is changed by another child. I found that I should use componentWillReceiveProps() and it works but with one click delay. What should I change to get immediate update?

I've posted my code on CodePen to make it easier to explain. If it's better to post it here directly please let me know, and I will update.

FCC: PomodoroClock - CodePen

Problems:

Is it possible to make it only with React or should I start learning Redux?

Edit: My code

import React from 'react';
import './PomodoroClock.scss';

class PomodoroClock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      sessionLength: 25,
      breakLength: 5,
    };
    this.handleTime = this.handleTime.bind(this);
    this.handleReset = this.handleReset.bind(this);
  }

  handleTime(type, time) {
    this.setState({
      [type]: time
    });
  }

  handleReset() {
    this.setState({
      sessionLength: 25,
      breakLength: 5,
    });
  }

  render() {
    return (
      <div className="container">
        <ClockSetter clockType="break" initialLength={this.state.breakLength} handleTime={this.handleTime} />
        <ClockSetter clockType="session" initialLength={this.state.sessionLength} handleTime={this.handleTime} />
        <Timer sessionLength={this.state.sessionLength} reset={this.handleReset}/>
        <span>TEST: State session - {this.state.sessionLength} State break - {this.state.breakLength}</span>
      </div>
    );
  }
}

class ClockSetter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      length: this.props.initialLength
    }
    this.handleDecrease = this.handleDecrease.bind(this);
    this.handleIncrease = this.handleIncrease.bind(this);
    this.refresh = this.refresh.bind(this);
  }

  handleDecrease() {
    if(this.state.length > 1) {
      this.setState ({
        length: this.state.length - 1
      });
    }
    this.props.handleTime(this.props.clockType+'Length', this.state.length - 1);
  }

  handleIncrease() {
    if(this.state.length < 60) {
      this.setState ({
        length: this.state.length + 1
      });
    }
    this.props.handleTime(this.props.clockType+'Length', this.state.length + 1);
  }

  refresh() {
    this.setState({
      length: this.props.initialLength
    });
  }

  componentWillReceiveProps(props) {
    if(this.state.length !== this.props.initialLength) {
      this.refresh();
    }
  }

  render() {
    let type = this.props.clockType;
    return(
      <div className="clock-setter">
        <div id={type + '-label'} className="first-letter-capitalize">{type + ' Length'}</div>
        <span id={type + '-decrement'} className="button" onClick={this.handleDecrease}>-</span>
        <span id={type + '-length'}>{this.state.length}</span>
        <span id={type + '-increment'} className="button" onClick={this.handleIncrease}>+</span>
      </div>
    );
  }
}

class Timer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      activeCountdown: 'Session',
      length: this.props.sessionLength
    }
    this.refresh = this.refresh.bind(this);
  }

  refresh() {
    this.setState({
      length: this.props.sessionLength
    });
  }

  componentWillReceiveProps(props) {
    if(this.state.length !== this.props.sessionLength) {
      this.refresh();
    }
  }

  render() {
    return(
      <div className="timer">
        <span id="timer-label">{this.state.activeCountdown}</span>
        <div id="time-left">{this.state.length}</div>
        <span id="start_stop" className="button">Start/Stop</span>
        <span id="reset" className="button" onClick={this.props.reset}>Reset</span>
      </div>
    );
  }
}


export default PomodoroClock;

Upvotes: 3

Views: 1729

Answers (1)

azium
azium

Reputation: 20624

Let's refactor your code in such a way that fixes the immediate issue while at the same time addresses some bad practices and antipatterns that will cause you headaches moving forward.

And because you're just starting, this is the perfect time to learn about hooks because they will make your code much more simple and easier to follow.

The main pitfall in your code is duplication of state. Let's start with the Timer component.

You are setting its initial state length to be the value of its parents state sessionLength. Even though you can perceive this to be some type of "initial" state and then afterwards the Timer's length will be independent of sessionLength once the countdown starts, this is not necessary. In fact.. duplication of state is not necessary in 99% of situations.

So what state should the Timer have? I would reckon that the timer might have its own internal counter state such that you display the current time like this.props.sessionLength - this.state.elapsedTime, but in your case the Timer isn't actually doing any timing. You're keeping track of the current time at the parent level anyways.

Knowing this.. what should the Timer state be? Nothing! The answer is no state. Timer can be a function, not a class, and receive props and display them.

function Timer(props) {
  return (
    <div className="timer">
      <span id="timer-label">Session</span>
      <div id="time-left">{props.sessionLength}</div>
      <span id="start_stop" className="button">
        Start/Stop
      </span>
      <span id="reset" className="button" onClick={props.reset}>
        Reset
      </span>
    </div>
  )
}

If that's all you change, this already solves your question.

Next let's look at the ClockSetter component. You are duplicating the state here in the exact same way, and not only that, you have extra handlers which simply call the parents handler handleTime, introducing extra steps and complexity which add unnecessary noise to your application. Let's get rid of the state and the extra handlers altogether, and as such we can use a function again, instead of a class:

function ClockSetter(props) {
  let type = props.clockType
  return (
    <div className="clock-setter">
      <div id={type + '-label'} className="first-letter-capitalize">
        {type + ' Length'}
      </div>
      <span
        id={type + '-decrement'}
        className="button"
        onClick={() => props.handleTime(type + 'Length', props.length - 1)}
      >
        -
      </span>
      <span id={type + '-length'}>{props.length}</span>
      <span
        id={type + '-increment'}
        className="button"
        onClick={() => props.handleTime(type + 'Length', props.length + 1)}
      >
        +
      </span>
    </div>
  )
}

I've inlined the onClick handlers for brevity. You could write the named handleDecrease and handleIncrease functions above the return statement and passed them the onClick if you want. That's just a matter of preference though.

*Note: the prop is now length not initialLength. Make sure to update that when rendering your ClockSetter components

For this last refactor I have updated your React cdn to point to the latest stable release 16.8.3, since it includes hooks.

Instead of using a class, let's write a normal function and use the React.useState hook. The code looks like this now:

function PomodoroClock() {
  let [sessionLength, setSessionLength] = React.useState(25)
  let [breakLength, setBreakLength] = React.useState(5)

  function handleReset() {
    setSessionLength(25)
    setBreakLength(5)
  }

  return (
    <div className="container">
      <ClockSetter
        clockType="break"
        length={breakLength}
        handleTime={setBreakLength}
      />
      <ClockSetter
        clockType="session"
        length={sessionLength}
        handleTime={setSessionLength}
      />
      <Timer
        sessionLength={sessionLength}
        reset={handleReset}
      />
      <span>
        Parent's state TEST: session - {sessionLength} break -
        {breakLength}
      </span>
    </div>
  )
}

and instead of having a single state object with keys that reference each timer, since that was the only way with stateful components before, we call useState twice each with their respective state and handlers. Now we can remove the type + Length argument in our ClockSetter component:

onClick={() => props.handleTime(props.length + 1)}

This is the entire program now:

function PomodoroClock() {
  let [sessionLength, setSessionLength] = React.useState(25)
  let [breakLength, setBreakLength] = React.useState(5)

  function handleReset() {
    setSessionLength(25)
    setBreakLength(5)
  }

  return (
    <div className="container">
      <ClockSetter
        clockType="break"
        length={breakLength}
        handleTime={setBreakLength}
      />
      <ClockSetter
        clockType="session"
        length={sessionLength}
        handleTime={setSessionLength}
      />
      <Timer
        sessionLength={sessionLength}
        reset={handleReset}
      />
      <span>
        Parent's state TEST: session - {sessionLength} break -
        {breakLength}
      </span>
    </div>
  )
}

function ClockSetter(props) {
  let type = props.clockType
  return (
    <div className="clock-setter">
      <div id={type + '-label'} className="first-letter-capitalize">
        {type + ' Length'}
      </div>
      <span
        id={type + '-decrement'}
        className="button"
        onClick={() => props.handleTime(props.length - 1)}
      >
        -
      </span>
      <span id={type + '-length'}>{props.length}</span>
      <span
        id={type + '-increment'}
        className="button"
        onClick={() => props.handleTime(props.length + 1)}
      >
        +
      </span>
    </div>
  )
}

function Timer(props) {
  return (
    <div className="timer">
      <span id="timer-label">Session</span>
      <div id="time-left">{props.sessionLength}</div>
      <span id="start_stop" className="button">
        Start/Stop
      </span>
      <span id="reset" className="button" onClick={props.reset}>
        Reset
      </span>
    </div>
  )
}

ReactDOM.render(
  <PomodoroClock />,
  document.getElementById('root')
);

Link to codepen

We've shaved of over 50 lines of code, it's much easier to read, and there's no potential issue of state being duplicated.

Hope that helps and happy coding! Please ask any questions if you need.

Upvotes: 2

Related Questions