How do I use React hooks and set states with useInterval

Viewed 170

I understand useInterval allows you to pass functional components as parameters, so I'm trying to take advantage of that and setting states with values used inside a functional component. In the following code, I want the ExecutionsPage to fetch projectScans and isProjectScansFetchComplete, which are returned from the useFetchProjectScansByUser() hook as an array:

1 const ExecutionsPage: React.FC = () => {
2     let user:string|null = sessionStorage.getItem('user')
3     const UPDATE_TABLE_TIMER: number = 30000; // Time (ms) until we call the API again to update the scans table
4     const [stateProjectScans, setStateProjectScans] = useState<Array<IProjectScan>>([]);
5     const [stateIsProjectScansFetchComplete, setStateIsProjectScansFetchComplete] = useState<boolean>(false);
6     function CallProjectScans(){
7         let [projectScans, isProjectScansFetchComplete] = useFetchProjectScansByUser(user)
8         return [projectScans, isProjectScansFetchComplete]
9     }
10    useEffect(() => {
11       let [projectScans, isProjectScansFetchComplete] = CallProjectScans()
12       // @ts-ignore
13        setStateProjectScans(projectScans)
14       // @ts-ignore
15        setStateIsProjectScansFetchComplete(isProjectScansFetchComplete)
16    }, [])
17    const GetProjectScans = () => {
18        let [projectScans, isProjectScansFetchComplete] = useFetchProjectScansByUser(user);
19        setStateProjectScans(projectScans)
20    };
21    // @ts-ignore
22    useInterval(GetProjectScans(), UPDATE_TABLE_TIMER)

In using this code, I'm getting this error: "Error: Too many re-renders. React limits the number of renders to prevent an infinite loop." So, with all that said, here are my questions:

  1. Readability-wise, am I using too many functions?
  2. The goal is to call the React hook on line 7 every 30 seconds and update the states on lines 4 and 5. Is there a better way to do this than how I'm currently attempting it?
  3. I tried using useEffect() to prevent the aforementioned error (by setting the states on the initial render of the ExecutionsPage, instead of rerendering it), but I am still getting it. Why?

Edit: Here's the function definition of useFetchProjectScansByUser():

export const useFetchProjectScansByUser = (user: string|null): [IProjectScan[], boolean] => {
    const [projectScans, setProjectScans] = useState<IProjectScan[]>([]);
    //update here
    const [isProjectScansFetchComplete, setIsProjectScansFetchComplete] = useState<boolean>(false);
    const {enqueueSnackbar, } = useSnackbar();
    
    useEffect(() => {
        ...
        /*function callJSONFunction(json) {
                let projectScans: IProjectScan[] = createProjectScansFromJSON(json);
                setProjectScans(projectScans);
        }*/
            const fetchProjectScans = async () => {
                try {
                    const response = await fetch(URL);
                    if (!response.ok) throw response.statusText;
            
                    const json = await response.json();
                    let projectScans: IProjectScan[] = createProjectScansFromJSON(json);
                    setProjectScans(projectScans);
                ...
                finally {
                    setIsProjectScansFetchComplete(true);
                }
            };
            fetchProjectScans();
        }
    }, []);
    return [projectScans, isProjectScansFetchComplete];
}
2 Answers

As expected, your hook uses state to store the results of the last call to the server.

If you want to modify/update that state, you should make your sever call in the useEffect fire again by adding a new state variable and passing it to the dependency array. You can then pass back to the hook caller a way to update that dependency array, so in effect you can control the update from outside the hook.

This is exactly how hooks should work: compartmentalize logic, and expose a simple API to the consumers of the hook.

export const useFetchProjectScansByUser = (user: string|null): [IProjectScan[], boolean] => {
    // make new state variable to control useEffect firing
    const [update, setUpdate] = useState(0)
    useEffect(() => {
        console.log("I will fire every time the update var changes")
        console.log("Instead of only on mount")
    }, [update]) // pass the state var to dep array
    // create a function that will update state reliably every call
    const updateFunc = () => setUpdate((v) => v+1)
    // pass it back to the hook consumer
    return [X, Y, updateFunc]
}

const MyComponent = () => {
     // now we have an update function to call from the consumer
     const [X, Y, update] = useFetchProjectScansByUser('user')

     useEffect(() => {
         // call the update function every 3 seconds, which will re-run the hook useEffect
         const id = setInterval(() => update(), 3000)
         return () => clearInterval(id)
     }, [])
     // if a hook state updates, this component which consumes that hook will re-render
     // thus X and Y will be updated.
     return <div>{X} {Y}</div>
}

One of the first rules of hooks is: Only Call Hooks at the Top Level

Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.

So, you'd need to change your code and move every React hook to the top. As stated in the docs you cannot even return earlier before another hook. Anyway, to fix the code then you need to remove the React hook useFetchProjectScansByUser from inside the functions.

Related