Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recursive `setTimeout` doesn't engage in expected way

I have a range input field with a numeric value which changes when I drag:

enter image description here

When I drag all the way to the right, the max value of the input increases. Then, when I release (onMouseUp or onTouchEnd), the max value decreases so that I can drag further and continue increasing max by dragging:

enter image description here

When I drag all the way to the left, the min value of the input decreases. Then, when I release (onMouseUp or onTouchEnd), the min value increases so that I can drag further and continue decreasing min by dragging:

enter image description here

I should always have a range of 99. For example, if I have increased the max value to 530, the min value will be 431.

PROBLEM:

I have two recursive setTimeouts set for the min and max values changing.

When the user first drags all the way to either side, the number should slowly change. If they hold it for 2 seconds, the number should increase more quickly. Relevant code:

// After arbitrary period, increase the rate at which the max value increments
this.fasterChangeStake = setTimeout(() => {
  this.stakeChangeTimeout = this.FAST_STAKE_CHANGE_TIMEOUT;
}, 2000);

This works the first time. But subsequent times, it latches onto the faster timeout first:

enter image description here

This is despite my clearing of timeouts when the drag ends:

clearTimeout(this.increaseStakeLimits);
clearTimeout(this.decreaseStakeLimits);
clearTimeout(this.timer);

Why is the first (slower) timeout not engaging?

Codepen: https://codepen.io/alanbuchanan/pen/NgjKMa?editors=0010

JS:

const {observable, action} = mobx
const {observer} = mobxReact
const {Component} = React

@observer
class InputRange extends Component {
  constructor() {
    super();
    this.INITIAL_STAKE_CHANGE_TIMEOUT = 200;
    this.FAST_STAKE_CHANGE_TIMEOUT = 20;
    this.SNAP_PERCENT = 10;
    this.increaseStakeLimits = this.increaseStakeLimits.bind(this);
    this.decreaseStakeLimits = this.decreaseStakeLimits.bind(this);
  }

  @observable min = 0;
  @observable max = 99;
  @observable stakeChangeTimeout = this.INITIAL_STAKE_CHANGE_TIMEOUT;
  @observable isIncreasing = false;
  @observable isDecreasing = false;
  @observable stake = 0;
  @action updateStake = (amount) => {
    if (amount > -1) {
      this.stake = amount
    }
  };

  increaseStakeLimits() {
    const { updateStake } = this;

    this.max = this.max += 1;
    this.min = this.max - 99
    updateStake(this.max);

    // After arbitrary period, increase the rate at which the max value increments
    this.fasterChangeStake = setTimeout(() => {
      this.stakeChangeTimeout = this.FAST_STAKE_CHANGE_TIMEOUT;
    }, 2000);

    // Recursive call, like setInterval
    this.timer = setTimeout(this.increaseStakeLimits, this.stakeChangeTimeout);
    this.isIncreasing = true;
  }

  decreaseStakeLimits() {
    console.warn('this.stake:', this.stake)
    const { stake } = this
    const { updateStake } = this;

    this.min = this.min -= 1;
    this.max = this.min + 99
    updateStake(this.min);

    // After arbitrary period, increase the rate at which the max value increments
    this.fasterChangeStake = setTimeout(() => {
      this.stakeChangeTimeout = this.FAST_STAKE_CHANGE_TIMEOUT;
    }, 2000);

    // Recursive call, like setInterval
    this.timer = setTimeout(this.decreaseStakeLimits, this.stakeChangeTimeout);
    this.isDecreasing = true;

  }

  handleStakeChange = e => {
    clearTimeout(this.increaseStakeLimits);
    clearTimeout(this.decreaseStakeLimits);
    clearTimeout(this.timer);

    const { updateStake } = this;
    const { stake } = this;

    const val = Number(e.target.value)

    // User has scrolled all the way to the right
    if (val >= this.max) {
      console.warn("scrolled to right")
      this.increaseStakeLimits();

    // User has scrolled all the way to the left
    } else if (val <= this.min) {
      console.warn("scrolled to left")
      if (val > -1) {
        this.decreaseStakeLimits();
      }
    } else {
      updateStake(val);
    }
  };

  handleRelease = () => {
    console.warn("RANGE:", this.max - this.min)
    console.warn("released");
    clearTimeout(this.fasterChangeStake);
    clearTimeout(this.timer);
    // Reset the timeout value to the initial one
    this.stakeChangeTimeout = this.INITIAL_STAKE_CHANGE_TIMEOUT;
    this.SNAP_PERCENT = 10
    const snapAmount = this.SNAP_PERCENT
    if (this.isIncreasing) {
      this.max += snapAmount
    }

    if(this.isDecreasing && this.min > 0) {
      this.min -= snapAmount
    }

    this.isIncreasing = false;
    this.isDecreasing = false;
  };

  render() {
    const { stake } = this;

    const style = {
      backgroundSize:
        (stake - this.min) * 100 / (this.max - this.min) + "% 100%"
    };

    return (
      <div className="rangeContainer">
        <div>{this.stake}</div>
        <div className="inputContainer">
          <input
            id="betRangeId"
            type="range"
            min={this.min}
            max={this.max}
            step="1"
            ref={input => {
              this.textInput = input;
            }}
            value={this.stake}
            onChange={this.handleStakeChange}
            onTouchEnd={this.handleRelease}
            onMouseUp={this.handleRelease}
            style={style}
          />
        </div>
      </div>
    );
  }
}

ReactDOM.render(<InputRange />, document.getElementById('root'))
like image 306
alanbuchanan Avatar asked Oct 29 '22 06:10

alanbuchanan


1 Answers

That's a really clever UI!

increaseStakeLimits() fires continuously the whole time the user holds the slider to the far right, so you're continually setting new setTimeouts to change this.stakeChangeTimeout to the shorter interval after two seconds (and continually setting new values for this.fasterChangeStake). These continue to fire even after handleRelease() tries to reset the interval to the longer value, which forces it back into fast mode until all of them have fired (the clearTimeout in handleRelease() only catches one of them.)

You can fix this by only setting that timeout once:

if (!this.fasterChangeStake) {
  this.fasterChangeStake = setTimeout(() => {
    this.stakeChangeTimeout = this.FAST_STAKE_CHANGE_TIMEOUT;
    this.fasterChangeStake = false; // <-- also do this in handleRelease() after clearing the timeout
  }, 2000);
}

https://codepen.io/anon/pen/OgmMNq?editors=0010

like image 121
Daniel Beck Avatar answered Nov 10 '22 00:11

Daniel Beck