Search code examples
typescriptreact-hooksuse-effect

How do I use React hooks and set states with useInterval


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];
}

Solution

  • 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>
    }