Before We Start
- Data fetching is a very simple task until it becomes complicated. Therefore I recommend that you use powerful data fetching libraries like TanStack Query, SWR, RTK Query.
- This post assumes that you are aware about what is API polling in RESTful architecture.
- All the code described is sort of a pseudo code, it's main purpose is to provide higher level understanding. The implementation might depend on the framework that you use.
With that said, let's get into it.
Polling (The Task)
So let's say that we have a requirement where the client(UI) requires frequent updates for a certain section but it doesn't necessarily need to be in real time, for that case we can utilise API polling.
setInterval() (The Intuitive Solution)
So we need a way to call an API endpoint repeatedly after a certain amount of interval(delay).
Which JavaScript function can we use for this? "psst...it's setInterval()"
API polling
We can achieve API polling by utilising setInterval()
to repeatedly call fetch function.
async function getData() {
try {
const response = await fetch(/*api endpoint*/);
/* handle response here */
} catch (error) {
/* handle error case here */
}
}
function apiPolling() {
setInterval(getData, delay);
}
/* Start Polling */
apiPolling();
the client will repeatedly call the endpoint after a delay between each call where the delay is equal to the desired polling interval.
Stop or Cancel polling
When we have to stop the polling at a certain condition or cancel the polling on user's request, we have to take care of two things.
- Clear the setInterval function from queue.
- Cancel any ongoing HTTP request that hasn't responded yet.
We can achieve this using clearInterval
and AbortController
.
let intervalId;
let controller;
async function getData() {
controller = new AbortController();
const signal = controller.signal;
try {
const response = await fetch(/*api endpoint*/,
{ signal }
);
/* handle response here */
} catch (error) {
/* handle error case here */
}
}
function apiPolling() {
intervalId = setInterval(getData, delay);
}
/* Start Polling */
apiPolling();
function clearPolling() {
clearInterval(intervalId);
if (controller) {
controller.abort();
}
}
/* To stop or cancel polling */
clearPolling()
we clear the polling queue by calling the clearInterval
function and making sure that any in-progress request is cancelled using AbortController
. You can read more about AbortController
Issues with setInterval()
Intuitively setInterval might look like the best solution for polling until you stop and think about how it works. It always calls the function repeatedly after a fixed delay. This is might and will cause a host of issues, since polling in never just about calling the endpoint again again but requires careful consideration on how to handle error cases and when to poll for the next request.
Some of the issues are:
- Network latency or Unresponsive server can cause the actual time to response to be greater than the polling interval, this can lead to network congestion and increased traffic on already struggling client or server.
- Variations in response time in conjunction with the above issue can cause multiple queued up requests that won't necessarily return in order, leading to a host of request handling and UX problems.
- Since setInterval isn't aware of the error case or stop condition, we have to manually stop the polling for every case and with the queued up request issue it might not be simple or even possible to cancel all those requests.
setTimeout() (The Proper Solution)
Intuitively setTimeout
might not look like a solution for the polling at first. After all it only calls the function once after the delay and done.
How can we use setTimeout for a task that requires a function to be called repeatedly? "Ahh...the answer is recursion".
We can recursively call setTimeout
to achieve polling and just by the virtue of how this solution works, we can improve upon the issues encountered while using setInterval and improve user experience as well as the developer experience (talking about DX, such an original of me).
Let's start with creating a recursive setTimeout
function that loops over itself and is invoked immediately.
(function loop() {
setTimeout(() => {
/* Function logic here */
loop();
}, delay);
})();
Now we can extract the function logic as a named function out of setTimeout
.
function loopingLogic() {
/* Function logic here */
loop();
}
(function loop() {
setTimeout(loopingLogic, delay);
})();
Let's look at what's happening here, at first the loop()
is called, it invokes a setTimeout
which waits for the delay and calls the loopingLogic()
, once all the tasks in that function are performed, at the end loop()
function is called again, creating a recursion which calls the loop()
after every execution of loopingLogic()
Now let's say we only want to loop over the function if a certain condition is met or to stop looping when that condition is not met. We can achieve that by checking if that condition is true and only then continue to loop.
function loopingLogic() {
/* Function logic here */
if (/* Condition is true */) {
loop();
}
}
(function loop() {
setTimeout(loopingLogic, delay);
})();
Now, as we have the base logic setup, let's look at how we can achieve API polling using this pattern.
async function getData() {
try {
const response = await fetch(/*api endpoint*/);
/* handle response here */
if (/* Condition is true */) {
apiPolling();
}
} catch (error) {
/* handle error case here */
}
}
function apiPolling() {
setTimeout(getData, delay);
}
/* Start Polling */
apiPolling();
Let's walkthrough the code. We can start polling by calling the apiPolling()
function, it invokes a setTimeout
which waits for the delay before calling the getData()
function.
In getData()
function we are handling the data fetching logic. We are doing few things in that function:
- We are hitting the api endpoint using fetch.
- We wait for the response and handle the response accordingly.
- We check if certain condition is met before calling the
apiPolling()
function to recursively hit the api endpoint for polling. - We catch any errors and handle those errors accordingly.
Now let's look at how this pattern of polling improves over setInterval
.
- We only poll for the next request when the response from the current request is received. This solves the problem of network latency and unresponsive server causing a network congestion, reduced overall traffic and mitigates the issue of race conditions caused by requests that won't necessarily return in order.
- By checking for the base case or guard clause in the if statement, we can make sure the polling continues only if that condition is met, making it very easy to automatically stop polling and preventing other issues that might arise when something errors out and helps in better error handling.
We can expand over our new polling pattern to allow users to gracefully stop or cancel polling.
let intervalId;
let controller;
async function getData() {
controller = new AbortController();
const signal = controller.signal;
try {
const response = await fetch(/*api endpoint*/,
{ signal }
);
/* handle response here */
if (/* Condition is true */) {
apiPolling();
}
} catch (error) {
/* handle error case here */
}
}
function apiPolling() {
intervalId = setTimeout(getData, delay);
}
function clearPolling() {
clearInterval(intervalId);
if (controller) {
controller.abort();
}
}
/* To stop or cancel polling */
clearPolling()
Conclusion
When presented with a task to create API polling, setInterval
might intuitively seem like the best option for this task. However as discussed above we can see that it is not the best option and can lead to some nasty issues and complicated code as we progress. Instead we can utilise the recursive setTimeout
pattern to create better a solution and prevent or even improve upon the issues that might arise by using setInterval
.
Remember, whenever you have to poll an API on a remote server, always go for recursive setTimeout
pattern.
Let me know in the comments below if you found this post helpful, any points that I missed and discuss what are the different ways you like to do API polling.
I like to talk about tech in general and events surrounding it. You can connect with me on Twitter/X and LinkedIn
Top comments (2)
Acho que poderia ter um tratamento para quando o próprio navegador finaliza os timers para economizar energia. No evento
visibilitychange
reativá-los.Can you please comment in English. I don't understand what I guess is Portuguese. Apologies from my end.