Polling with Redux Sagas
When developing an app, you may run into a case where you need to wait for a long running process to complete. A common pattern for dealing with such a case is polling. In this pattern data is repeatedly requested from a source, such an an API, at intervals. Usually, polling continues until the long running process is completed or cancelled.
For this tutorial we will consider the use case of ordering a pizza. Many pizza delivery sites now offer order tracking which update you on the progress of pizza from order submission to the moment it is delivered at your door. This tutorial uses a very simplified version of this process to show how you can use a polling pattern with Redux Sagas to update your user interface as the order progresses.

You can download the sample project for this demo from https://github.com/supertorio/redux-sagas-polling-tutorial
Note: the sample project was set up using CRA with a template for Typescript + Redux-toolkit, but you could just as easily accomplish this without typescript and using basic redux. I assume you already have familiarity with using redux and sagas and won’t be covering any project setup.
Breaking the Problem Down
When the user clicks the submit order button, we expect that an action is dispatched with a connected saga side effect. Inside that initial saga:
// 1. The order is submitted to an api and receives back an order number and the initial order status.
// 2. The store is updated with the new information.
// 3. Call pizzaStatusWatchWorker saga to begin polling
1// Default Saga2export function* orderSaga() {3 yield takeLatest(actions.submitOrder.type, submitOrder)4}56export function* submitOrder(action: PayloadAction<string>) {7 try {8 // 1. Submit Order9 const { orderNumber, orderStatus } = yield call(10 mockPostOrderAPI,11 action.payload12 )1314 //2. Process Response15 yield put(actions.submitOrderComplete({ orderNumber, orderStatus }))1617 //3. Start Polling18 yield call(pizzaStatusWatchWorker)19 } catch (error) {20 yield put(21 actions.submitOrderComplete({ orderNumber: "", orderStatus: "Error" })22 )23 }24}
The polling watch worker saga (pizzaStatusWatchWorker) utilises the racing effects concept. This allows for the start of two simultaneous processes. When one process is completed, the other is automatically cancelled. For this race we need to:
// 4. Trigger a call effect called ‘task’ to start a polling task. The pizzaStatusPollingWorker saga runs as an infinite loop, so it will never finish before the other process.
// 5. Start a take effect called ‘cancel’ which waits and looks for the cancel action to be dispatched.
1// Name of action which ends the polling2const CANCEL_STATUS_POLLING = "CancelStatusPolling"34export function* pizzaStatusWatchWorker() {5 // Race starts two concurrent effects. We start our polling effect 'task'. As soon6 // as the take effect 'cancelled' is triggered, the 'task' will be cancelled.7 yield race({8 //4. Start the polling worker9 task: call(pizzaStatusPollingWorker),10 //5. Start a take effect waiting for the cancel action.11 cancel: take(CANCEL_STATUS_POLLING)12 })13}
Finally in the pizzaStatusPollingWorker, is where the work of getting the next status and updating the store at an interval is accomplished
// 6. The contents are wrapped in a while statement that should run indefinitely
// 7. Fetch the new status from the API using the current order number
// 8. Update the store with new status
// 9. Check if a status is encountered that triggers an end to the polling
// 10. Dispatch the cancel action if the polling is done
// 11. Delay before the next polling iteration
1// Time between polling iterations in MS.2const POLLING_DELAY = 250034export function* pizzaStatusPollingWorker() {5 // 6. Run indefinitely6 while (true) {7 try {8 // 7. Fetch the new status from the API using the current order number9 const currentOrderNumber: string = yield select(selectOrderNumber)10 const newStatus: OrderStatus = yield call(11 mockOrderStatusAPI,12 currentOrderNumber13 )1415 // 8. Update the store with new status16 yield put(actions.updateOrderStatus(newStatus))1718 // 9. Check if a status is encountered that triggers an end to the polling19 if (newStatus === "Complete" || newStatus === "Error") {20 // 10. Dispatch the cancel action if the polling is done21 yield put({ type: CANCEL_STATUS_POLLING })22 } else {23 // 11. Otherwise, delay before the next polling iteration24 yield delay(POLLING_DELAY)25 }26 } catch (error) {27 // Catch any unexpected errors and cancel polling if something goes wrong.28 console.error(error)29 yield put({ type: CANCEL_STATUS_POLLING })30 yield put(actions.updateOrderStatus("Error"))31 }32 }33}
These three sagas work together to provide a clean and testable way to achieve the polling pattern in your redux app.
What’s Next?
As you can see, with a couple built in saga effects, its easy and straightforward to implement a polling pattern in your redux-sagas app. You can extend this technique further to solve for a wide variety of use cases where keeping your client app and server in sync is critical.