#react#redux#redux-saga

Exploring the power of Redux-Saga by implementing cancellable polling.

Syed Sibtain and Jawakar Durai's avatar

Syed Sibtain and Jawakar Durai

Understanding state management and handling different flows of asynchronous calls with it is one of the most challenging aspects of developing an application for a front-end developer. To handle data fetching, we can leverage an existing state management library using a middleware. And this is where middlewares like Redux-Thunk and Redux-Saga come to the rescue.

Even though Redux Thunk is well suited for most of our asynchronous action needs, Sometimes it could become more complex as our business logic gets more complex. In those tough situations, we can rely on Redux Saga.

Redux Saga is a middleware library that allows a Redux store to interact with resources outside of itself asynchronously. This includes making HTTP requests to external services, accessing browser storage, etc. These operations are also referred to as side effects. Redux Saga assists in handling these side effects in a more manageable manner.

Here is an illustration of how middleware flow works.

Flow-Chart

What problem are we trying to resolve here?

When developing applications, there can be situations where we must wait for some process to be completed and our frontend needs to be notified.

As an example, consider paying for a cart, we need to wait for a response from backend to redirect user from payment page to order confirmation page. And to deal with such situations, we can embrace technologies like Websockets. When that's not available as an option, we implement a technique known as Polling.

From a JavaScript perspective, polling can be defined as periodically making API calls to the back-end and checking the response of the same. And it keeps going until we cancel it based on its status.

The Scenario

Consider a scenario where on a button click we must begin polling and enable a user to view the results of an API call every 3 seconds. And when the user clicks on the Stop Polling button, we should be able to stop the polling.

The Solution

Let's move on to the code as we will be putting a solution to the same issue into practise in this article. We'll start by building a React application in which we will fetch data from Random Quote Generator API.

npx create-react-app <project-name>
cd <project-name>

Now we will install all of the project's dependencies, viz. redux-toolkit, redux-saga and react-redux.

npm i @reduxjs/toolkit
npm i redux-saga
npm i react-redux

The normal method will involve calling the API directly from within our component code, however in this case we execute a redux action and shift the data fetching mechanism to a saga.

// actions.js
//action creators
export const startPolling = () => {
    return {
        type: "START_POLLING",
    };
};
 
export const stopPolling = () => {
    return {
        type: "STOP_POLLING",
    };
};

Now let's configure our reducers, which will, in response to an action, return a state. Additionally, we can include the loading and polling states.

// reducer.js
const initialState = {
    quotes: [],
    loading: false,
    polling: false,
};
 
export const quotePollReducer = (state = initialState, action) => {
    switch (action.type) {
        case "START_POLLING":
            return { ...state, loading: true, polling: true };
        case "GET_QUOTES_SUCCESS":
            return { ...state, loading: false, quotes: action.quotes };
        case "STOP_POLLING":
            return { ...state, polling: false }
        default:
            return state
    }
};
 

I know that everyone wants to see the magic work and the Redux-Saga begin. However, before we do so, we must first setup the component that will call the API and retrieve the data for us. Let's do that first.

// quote.js
// Data Fetching Component
const url = "<Your API>";
 
export const fetchQuoteData = () => {
    return fetch(url)
      .then((response) => response.json())
      .catch((error) => {
        throw error;
      });
  };
 

Redux Saga Setup

A few things need to be discussed with you before we move on to the Redux-Saga Polling. “Sagas are implemented as generator functions that yield objects to the redux-saga middleware.”

And the saga is divided into two parts. One is the watcher function, while the other is the worker function.

The Watcher Saga takes care of every action that is dispatched to the redux store, and if it matches the action it is supposed to handle, it will allocate it to its worker saga. The worker saga handles the action and completes all of the tasks, as well as the expected side effects.

Now, let's get into the code.

// saga.js
import { call, put, take, fork, cancel, cancelled, delay } from "redux-saga/effects";
import { fetchQuoteData } from "./quote.js"
 
//Worker Function
//This creates a loop in which a request is made with a delay after the API call returns.
function* quoteStatusCheckLoop() {
    while (true)
        try {
            const quotes = yield call(fetchQuoteData)
            yield put({ type: "GET_QUOTES_SUCCESS", quotes: quotes });
            yield delay(3000)
            //adds a delay of 3 seconds
 
        } finally {
            if (yield cancelled()) {
            }
        }
}
 
export function* quotePollSaga() {
    while (yield take("START_POLLING") {
 
        // starts the task in the background
        const quotePollTask = yield fork(quoteStatusCheckLoop)
        // Fork: makes a non-blocking call to a function that produces a promise.
 
        // wait for the user stop action (button click in our case)
        yield take("STOP_POLLING")
 
        // user clicked stop. cancel the background task
        // this will cause the forked quotePollTask task to jump into its finally block
        yield cancel(quotePollTask)
        // Cancel: cancels the saga execution.
    }
}
 

Note: A Blocking call indicates that the Saga has yielded an Effect and will wait for the result of its execution before proceeding to the next instruction inside the yielding Generator. Example (take: Waits for an action, call: Waits for the promise to resolve)

A non-blocking call indicates that the Saga will resume immediately and it will not wait. Example (fork: Will not wait for other Saga)

Let's finally set up our store so that we can execute everything.

//store.js
import createSagaMiddleware from "redux-saga";
import { configureStore } from "@reduxjs/toolkit";
import { quotePollReducer } from "./reducer.js";
import { quotePollSaga } from "./saga.js";
 
const sagaMiddleware = createSagaMiddleware();
 
export const store = configureStore({
    reducer: quotePollReducer,
    middleware: () => [sagaMiddleware]
});
 
sagaMiddleware.run(quotePollSaga);
 

And this is the user interface component from which we will dispatch the action when a button is clicked.

// App.js
import { startPolling, stopPolling } from "./actions.js";
import { useSelector, useDispatch } from "react-redux";
 
function App() {
  const dispatch = useDispatch();
  const quotes = useSelector((state) => state.quotes);
  const loading = useSelector((state) => state.loading);
 
  return (
    <>
      {loading && <h2>Loading...</h2>}
        <Card>
            <p>{quotes.content}</p>
            <p> {quotes.author} </p>
            <button onClick={()=> dispatch(startPolling())}>Start Polling</button>
            <button onClick={()=> dispatch(stopPolling())}>Stop Polling</button>
        </Card>
    </>
  );
}
 
export default App;
 

Also, in index.js we need to wrap our App.js in Provider

    <Provider store={store}>
      <App />
    </Provider>
 

The flow

a. When the user clicks the Start Polling button, the action startPolling will be dispatched.

b. Once it has been dispatched, the take("START_POLLING) in the watcher saga quotePollSaga will be yielded, which will trigger the background polling by forking in fork(quoteStatusCheckLoop) and we get the reference to the fork in quotePollTask.

c. On the background: The polling will begin in the worker saga quoteStatusCheckLoop, which will run the while statement in an infinite loop. For each while loop; 1) it will call fetchQuoteData using a call effect which will resume the execution once it resolves the promise with response. 2) The store will be updated using the GET_QUOTES_SUCCESS action. 3) And then wait for 3 seconds.

d. Now back to quotePollSaga: We started the fork and now we're waiting for the action STOP_POLLING. The action is dispatched when the user clicks on the Stop Polling button. This line will yield yield take("STOP_POLLING") and resume the generator to the next line.

e. When a user clicks the Stop Polling button, the action stopPolling is dispatched, and the next line is yield cancel(quotePollTask) will be triggered, which cancels the running background task.

f. Lastly, back to the background task: Once cancelled, whatever is running in the task, even if we're waiting for the API response, will be cancelled and the execution will be jumped to the finally block. We can handle the cancelled task by using yield cancelled(). But we do not need to handle it.

Brownie Point

Instead of using buttons, we can also use the useEffect hook to start polling when a component mounts and terminate polling when a component unmounts.

useEffect(() => {
    dispatch(startPolling())
 
      return () => {
          dispatch(stopPolling());
        // cancel the polling when component unmounts
}, [dispatch])

Conclusion

Thanks to a few in-built saga effects, implementing a polling pattern in our apps is straightforward and declarative. By transferring API polling from the component to the saga, it is feasible to separate concerns for handling APIs and rendering data into different files, which helps facilitate problem-solving. Here is the app we built together Redux-Saga Polling App Demo and Github

Quick overview of the effects we discussed above:

  1. call: run a method, Promise or other Saga.
  2. cancel: cancels the saga execution.
  3. put: dispatch an action into the store.
  4. take: wait for a redux action/actions to be dispatched into the store.
  5. delay: block execution for a predefined number of milliseconds.
  6. fork: performs a non-blocking call to a generator or a function that returns a promise.

Additional References

  1. Task Cancellation
  2. Blocking and Non Blocking Call
  3. Generators In Javascript