React: Infinite function call loop after Slider onChangeEnd

When I change the value of my slider, it should call a function onChangeEnd. However, when this occurs, the calculateEvent function is called in an infinite loop. This continues forever, even as I keep moving my slider around afterwards. Why does this happen?

var yearStart = 0;
var yearEnd = 50;
var years : number[] = [];
while(yearStart < yearEnd+1){
    years.push(yearStart++);
}
const xAxis : number[] = years;

const Savings = () => {

    const [initialSavings, setInitialSavings] = React.useState<string>("5000");
    const [monthlyDepo, setMonthlyDepo] = React.useState<string>("100");
    const [interestRate, setInterestRate] = React.useState<number>(2);
    const [yAxis, setYAxis] = React.useState<number[]>([]);

    const handleChange = useCallback((newValue) => {
        setInterestRate(newValue);
      }, []);

    const calculateEvent = useCallback((event : string, slider : number, option : number) => {
        switch(option) {
            case 1:
              setInitialSavings(event);
              break;
            case 2:
              setMonthlyDepo(event);
              break;
            case 3:
              setInterestRate(slider);
          }
        console.log("Calculate event called with slider value: ", interestRate);
        getProjection(initialSavings, monthlyDepo, interestRate).then((m) => {
            console.log(m);
            setYAxis(m);
        })
    }, [initialSavings, monthlyDepo, interestRate]);

    return(
    <DefaultLayout>
        <Container pt={6}>
            <VStack spacing={4}>
                <Heading as="h1">Interest Rate Calculator</Heading>
                <Heading as="h4">{initialSavings}, {monthlyDepo}, {interestRate}</Heading>
                <Input 
                    label="Initial Savings amount" 
                    name="Initial Savings" 
                    onInput={e => calculateEvent(e.currentTarget.value, 0, 1)} 
                    placeholder="5000"
                />
                <Input 
                    label="Monthly Deposit" 
                    name="Monthly Deposit" 
                    onInput={e => calculateEvent(e.currentTarget.value, 0, 2)} 
                    placeholder="100"
                />
                <Slider
                    label="Interest Rate"
                    name="Interest Rate"
                    value={interestRate}
                    min={1}
                    max={15}
                    step={1}
                    onChange={handleChange}
                    onChangeEnd={e => calculateEvent("", e, 3)} 
                    focusThumbOnChange={false}
                    role="group"
                    onFocus={(e) => {
                        e.preventDefault();
                    }}
                />
                <LineChart
                    title="Savings Over time"
                    xAxisData={xAxis}
                    yAxisData={yAxis}
                    xLabel="Years"
                    yLabel="Amount"
                />
            </VStack>
        </Container>
    </DefaultLayout>
    )
}

Answer

Okay, heres what I propose.

Let’s break that calculateEvent function up into its separate components.

Then, let’s create a useEffect callback that will re-calculate the projection, whenever our inputs change. I’d prefer to do this on render, but we are stuck with an effect because getProjection returns a promise.

Now, I understand why you want to use the onChangeEnd event. getProjection() is expensive, and so you only want to call it, when the user has finished choosing a value.

So let’s break it up into two states. One state to manage the slider value. And then a second state to hold the actual interestRate value. We will wait to update the interestRate value until onChangeEnd is called.

Also, let’s make sure to control those inputs by putting values on them, and using onChange instead of onInput.

var yearStart = 0;
var yearEnd = 50;
var years: number[] = [];
while (yearStart < yearEnd + 1) {
  years.push(yearStart++);
}
const xAxis: number[] = years;

const Savings = () => {
  const [initialSavings, setInitialSavings] = React.useState<string>('5000');
  const [monthlyDepo, setMonthlyDepo] = React.useState<string>('100');
  const [interestRate, setInterestRate] = React.useState<number>(2);
  const [interestRateSlider, setInterestRateSlider] = React.useState<number>(2);
  const [yAxis, setYAxis] = React.useState<number[]>([]);

  const handleSavingsChange = (e) => {
    setInitialSavings(e.target.value);
  }

  const handleMonthlyDepoChange = (e) => {
    setMonthlyDepo(e.target.value);
  }

  const handleInterestRateSliderChange = (value) => {
    setInterestRateSlider(value);
  };

  const handleInterestRateSliderChangeEnd = (value) => {
    setInterestRate(value)
  }

  useEffect(() => {
    getProjection(initialSavings, monthlyDepo, interestRate).then((m) => {
      setYAxis(m);
    });
  },[initialSavings, monthlyDepo, interestRate])

  return (
    <DefaultLayout>
      <Container pt={6}>
        <VStack spacing={4}>
          <Heading as="h1">Interest Rate Calculator</Heading>
          <Heading as="h4">
            {initialSavings}, {monthlyDepo}, {interestRate}
          </Heading>
          <Input
            label="Initial Savings amount"
            name="Initial Savings"
            value={initialSavings}
            onChange={handleSavingsChange}
            placeholder="5000"
          />
          <Input
            label="Monthly Deposit"
            name="Monthly Deposit"
            value={monthlyDepo}
            onChange={handleMonthlyDepoChange}
            placeholder="100"
          />
          <Slider
            label="Interest Rate"
            name="Interest Rate"
            value={interestRate}
            min={1}
            max={15}
            step={1}
            onChange={handleInterestRateSliderChange}
            onChangeEnd={handleInterestRateSliderChangeEnd}
            focusThumbOnChange={false}
            role="group"
            onFocus={(e) => {
              e.preventDefault();
            }}
          />
          <LineChart
            title="Savings Over time"
            xAxisData={xAxis}
            yAxisData={yAxis}
            xLabel="Years"
            yLabel="Amount"
          />
        </VStack>
      </Container>
    </DefaultLayout>
  );
};