Hen6
Hen6

Reputation: 51

Reactjs - SetTimeout with state change re-render

I'm currently running into some issues whilst developing a Typescript React App. Underneath is my current code..

But it's not behaving like I would want it to behave. :)

So what I would like to achieve is that the data with getData(depth) runs whenever the component is being loaded and afterwards every 5 seconds.

But when the Depth changes with the Dropdown.item buttons, it should re-render and the getData() should be ran with the new depth value that we just set in the state.. and keep on rendering afterwards with the new value...

I've been struggling with this, so any help is very much appreciated!!

Thank you!

import React, { useState, useEffect } from "react";

const chart = () => {
  const [depth, setDepth] = useState(20);
  const [chartData, setChartData] = useState({})

//Getting the data when the app initially renders and should keep rendering every 5 seconds after that.
//When the value of the depth changes, we should stop getting the data with the old depth //value and should start a new interval of 5 seconds and just keep running with the new //depth value
//When first entering the app, this should run immediately with the initial depth state //(20)

  useEffect(() => {
    const interval = setInterval(() => {
     //this code is not the actual code, just an example of what is running
      const data = getData(depth)
     //just fetched the new data, now setting it.. 
      setChartData(data)
    }, 5000);
    return () => clearInterval(interval);
  }, []);

  return (
<div>
  <div>
   <DropdownButton id="dropdown-basic-button" title="Depth Percentage">
    <Dropdown.Item onClick={() => setDepth(5)}>5%</Dropdown.Item>
    <Dropdown.Item onClick={() => setDepth(20)}>20%</Dropdown.Item>
   </DropdownButton>
  </div>
  <div>
   //Rendering the Chart here....
  </div>
</div>
  );
};

export default chart;

Upvotes: 3

Views: 4884

Answers (2)

Louay Al-osh
Louay Al-osh

Reputation: 3415

That's because useEffect hook takes a second parameter called dependency array, where this dependency array is what matters for the inner callback(inside useEffect) to access the latest values you want.

So you are not being truthful here, if the inner callback depends on depth to be in its latest update then you should include it in the dependency array

useEffect(() => { ... }, [ depth ]);

that's for the depth but writing this code will immediately cause problems because for each new depth value the inner callback will be called and the setInterval will re-run again (causing many many...many intervals).

To solve this you should avoid using setInterval altogether in hooks based code.

If having intervals is important I have a suggestion for you


  const [intervalCount, setIntervalCount] = useState(0);
  const [depth, setDepth] = useState(20);
  const [chartData, setChartData] = useState({})


  useEffect(() => {
    // when depth change re-fetch data and set it
    const data: any = getData(depth);
    setChartData(data);
  }, [depth])


  // simulate set interval behavior 
  // each 5 s this function will be re-invoked
  useEffect(() => {

    // re-fetch data and set it
    const data: any = getData(depth);
    setChartData(data);

    // wait 5 s before cause a re-render
    setTimeout(() => {
      setIntervalCount(count => count + 1);
    }, 5000);

  }, [intervalCount]);



Updated: After reading from Dan Abramov blog

you can find a better elegant solution that uses setInterval and hooks

Making setInterval Declarative with React Hooks

He made a custom hook called useInterval

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

Usage be like

  useInterval(() => {
    // Your custom logic here
    setCount(count + 1);
  }, 1000);

Upvotes: 2

Swell_boy
Swell_boy

Reputation: 31

You have two problems inside your code:

  1. Micro tasks can see values only in the scope of the callback function and capture rest on the first call only;
  2. You need to inform react that should recall your fetch function with the updated values.

So your code should become (if you want to do it that way):

Edit on code sandbox

import React, { useState, useEffect, useRef } from "react";

const Chart = () => {
  const [depth, setDepth] = useState(20);
  const [data, setData] = useState([]);

  //Getting the data when the app initially renders and should keep rendering every 5 seconds after that.
  //When the value of the depth changes, we should stop getting the data with the old depth //value and should start a new interval of 5 seconds and just keep running with the new //depth value
  //When first entering the app, this should run immediately with the initial depth state //(20)
  const intervalRef = useRef(null);
  useEffect(() => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
    }
    intervalRef.current = setInterval(() => {
      //this code is not the actual code, just an example of what is running
      const dataValue = { depth };
      console.log("Captured dataValue in the interval:", dataValue);
      setData((prev) => [...prev, dataValue]);
      //just fetched the new data, now setting it..
    }, 5000);
    return () => clearInterval(intervalRef.current);
  }, [depth]);

  return (
    <div>
      <div>
        <button onClick={() => setDepth(5)}>5%</button>
        <button onClick={() => setDepth(20)}>20%</button>
        <ul>
          {data.map((value, index) => (
            <li key={index}>Current depth for fetch: {value.depth}%</li>
          ))}
        </ul>
      </div>
    </div>
  );
};

export default Chart;

Upvotes: 0

Related Questions