Polling API every x seconds with react
Asked Answered
L

7

87

I have to monitoring some data update info on the screen each one or two seconds. The way I figured that was using this implementation:

    componentDidMount() {
        this.timer = setInterval(()=> this.getItems(), 1000);
      }
    
      componentWillUnmount() {
        this.timer = null;
      }
    
      getItems() {
        fetch(this.getEndpoint('api url endpoint'))
            .then(result => result.json())
            .then(result => this.setState({ items: result }));
      }

Is this the correct approach?

Laudianism answered 10/9, 2017 at 12:37 Comment(4)
This is a way to do it, but it's inefficient and will overload your server when you scale your app. If you use a socket connection, you can be notified when messages arrive, which will be much more efficientUnwearied
I just have the REST API to consume... how can I make this "pooling" in a properly way?Laudianism
It depends on what technology you have in your server. You should do some reading on how web sockets work, There is an article here blog.teamtreehouse.com/an-introduction-to-websockets but there are plenty of resources around to play withUnwearied
If it's working, leave it.Dansby
T
77

Well, since you have only an API and don't have control over it in order to change it to use sockets, the only way you have is to poll.

As per your polling is concerned, you're doing the decent approach. But there is one catch in your code above.

componentDidMount() {
  this.timer = setInterval(()=> this.getItems(), 1000);
}

componentWillUnmount() {
  this.timer = null; // here...
}

getItems() {
  fetch(this.getEndpoint('api url endpoint'))
    .then(result => result.json())
    .then(result => this.setState({ items: result }));
}

The issue here is that once your component unmounts, though the reference to interval that you stored in this.timer is set to null, it is not stopped yet. The interval will keep invoking the handler even after your component has been unmounted and will try to setState in a component which no longer exists.

To handle it properly use clearInterval(this.timer) first and then set this.timer = null.

Also, the fetch call is asynchronous, which might cause the same issue. Make it cancelable and cancel if any fetch is incomplete.

I hope this helps.

Thoria answered 22/8, 2018 at 18:21 Comment(3)
It would be nice if you demonstrated how to make the changes you propose.Hearst
Careful with async calls using setInterval() as it will call even if it's waiting for the API to respond. A safer call would be using setTimeout() with recursion.Reservation
A working example of @GustavoGarcia's comment is added here - https://mcmap.net/q/237796/-polling-api-every-x-seconds-with-reactBunch
D
69

Although an old question it was the top result when I searched for React Polling and didn't have an answer that worked with Hooks.

// utils.js

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

export const useInterval = (callback, delay) => {

  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);


  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      const id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

Source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/

You can then just import and use.

// MyPage.js

import useInterval from '../utils';

const MyPage = () => {

  useInterval(() => {
    // put your interval code here.
  }, 1000 * 10);

  return <div>my page content</div>;
}
Dd answered 2/3, 2020 at 23:34 Comment(6)
How do we cancel useInterval ? I mean stop the polling on any conditionZygospore
Setting the time to 0 cancels the timerTrailblazer
how to make this in typescript?Obsequies
Typescript NPM package for this hook: npmjs.com/package/@use-it/intervalDebrahdebrecen
How to cancel the timer once I get the response I wanted after polling? Tried to add 0 in delay. It makes continuous calls to the API.Panne
Pass in null. In mine I also treat 0 as null to avoid DDOSing my APIs on accident.Latrinalatrine
B
11

You could use a combination of setTimeout and clearTimeout.

setInterval would fire the API call every 'x' seconds irrespective whether the previous call succeeded or failed. This can eat into your browser memory and degrade performance over time. Moreover, if the server is down, setInterval would continue to bombard the server not knowing its down status.

Whereas,

You could do a recursion using setTimeout. Fire a subsequent API call, only if the previous API call succeed. If previous call has failed, clear the timeout and do not fire any further calls. if required, alert the user on failure. Let the user refresh the page to restart this process.

Here is an example code:

let apiTimeout = setTimeout(fetchAPIData, 1000);

function fetchAPIData(){
    fetch('API_END_POINT')
    .then(res => {
            if(res.statusCode == 200){
                // Process the response and update the view.
                // Recreate a setTimeout API call which will be fired after 1 second.
                apiTimeout = setTimeout(fetchAPIData, 1000);
            }else{
                clearTimeout(apiTimeout);
                // Failure case. If required, alert the user.
            }
    })
    .fail(function(){
         clearTimeout(apiTimeout);
         // Failure case. If required, alert the user.
    });
}
Bunch answered 28/7, 2020 at 12:45 Comment(0)
K
5

@AmitJS94, there's a detailed section on how to stop an interval that adds onto the methods that GavKilbride mentioned in this article.

The author says to add a state for a delay variable, and to pass in "null" for that delay when you want to pause the interval:

const [delay, setDelay] = useState(1000);
const [isRunning, setIsRunning] = useState(true);
  useInterval(() => {
    setCount(count + 1);
  }, isRunning ? delay : null);

    useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);

Definitely read the article to get a better understanding of the details -- it's super thorough and well-written!

Kali answered 21/6, 2020 at 0:24 Comment(0)
T
2

As Vasanth mention, I preferred to:

import { useEffect, useRef } from 'react';

export const useInterval = (
  callback: Function,
  fnCondition: Function,
  delay: number,
) => {
  const savedCallback = useRef<Function>();
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);
  useEffect(() => {
    let id: NodeJS.Timeout;
    const tick = async () => {
      try {
        const response =
          typeof savedCallback.current === 'function' &&
          (await savedCallback.current());
        if (fnCondition(response)) {
          id = setTimeout(tick, delay);
        } else {
          clearTimeout(id);
        }
      } catch (e) {
        console.error(e);
      }
    };
    tick();
    return () => id && clearTimeout(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [delay]);
};

WORKS: Using fnCondition inside which can be a condition based on the response from the last request.

//axios-hooks
const {
    data,
    isLoadingData,
    getData,
} = api.useGetData();

const fnCondition = (result: any) => {
    const randomContidion = Math.random();
    //return true to continue
    return randomContidion < 0.9;
  };
useInterval(() => getData(), fnCondition, 1000);

DOES NOT WORK: Passing delay as null to stop useInterval like this does not work for me with this code: https://www.aaron-powell.com/posts/2019-09-23-recursive-settimeout-with-react-hooks/

(You might get the impression it works, but after a few starts/stops it breaks)

  const [isRunning, setIsRunning] = useState(true);
  const handleOnclick = () => {
    setIsRunning(!isRunning);
  };

  useInterval(() => getData(), isRunning ? 1000 : null);
  <button onClick={handleOnclick}>{isRunning ? 'Stop' : 'Start'}</button>

Sum up: I'm able to stop useInterval by passing fnCondition, but not by passing delay=null

Terrenceterrene answered 24/8, 2021 at 7:56 Comment(1)
Do you know why you save the callback in a ref ?Awning
G
2

Here is a simple example using hooks in function component and this will refresh your data in a set interval.

import React from 'react';

import { useEffect, useState } from 'react';

export default function App() {
  let [jokes, setJokes] = useState('Initial');

  async function fetchJokes() {
    let a = await fetch('https://api.chucknorris.io/jokes/random');
    let b = await a.json();
    setJokes(b.value);
  }

// Below function works like compomentWillUnmount and hence it clears the timeout
  useEffect(() => {
    let id = setTimeout(fetchJokes, 2000);
    return () => clearTimeout(id);
  });

  return <div>{jokes}</div>;
}

or, you can use axios as well to make the API calls.

function App() {
  const [state, setState] = useState("Loading.....");

  function fetchData() {
    axios.get(`https://api.chucknorris.io/jokes/random`).then((response) => {
      setState(response.data.value);
    });
  }

  useEffect(() => {
    console.log("Hi there!");
    let timerId = setTimeout(fetchData, 2000);
     return ()=> clearInterval(timerId); 
  });

  return (
    <>
      This component
      <h3>{state}</h3>
    </>
  );
}
Geri answered 17/11, 2022 at 18:45 Comment(1)
This is the correct solution. You should never use setInterval for polling purposes as you can quickly get into race conditions.Predilection
B
0

Here's a simple, full solution, that:

  • Polls every X seconds

  • Has the option of increasing the timeout each time the logic runs so you don't overload the server

  • Clears the timeouts when the end user exits the component

     //mount data
     componentDidMount() {
         //run this function to get your data for the first time
         this.getYourData();
         //use the setTimeout to poll continuously, but each time increase the timer
         this.timer = setTimeout(this.timeoutIncreaser, this.timeoutCounter);
     }
    
     //unmounting process
     componentWillUnmount() {
         this.timer = null; //clear variable
         this.timeoutIncreaser = null; //clear function that resets timer
     }
    
     //increase by timeout by certain amount each time this is ran, and call fetchData() to reload screen
     timeoutIncreaser = () => {
         this.timeoutCounter += 1000 * 2; //increase timeout by 2 seconds every time
         this.getYourData(); //this can be any function that you want ran every x seconds
         setTimeout(this.timeoutIncreaser, this.timeoutCounter);
     }
    
Bolte answered 8/10, 2020 at 18:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.