Redux Saga is a library that is commonly used to handle side effects in Redux. When an action is fired in a Redux application, it triggers a change in the app’s state. In some cases, you may need to perform additional tasks that are not directly related to the state change or are asynchronous. Redux Saga helps you manage these side effects in a clean and organized way.
In this blog, we will cover the following topics:
- When to use Redux Saga
- Basic example of using Redux Saga
- How Redux Saga works behind the scenes
- Basic helpers provided by Redux Saga
takeEvery()
takeLatest()
take()
put()
call()
- Running effects in parallel using
all()
andrace()
Let’s dive into the details below.
When to use Redux Saga
In a Redux application, when an action is dispatched, something changes in the app’s state. There are cases where you may need to perform additional tasks that are not directly related to the state change. These tasks can include making HTTP calls to a server, sending WebSocket events, fetching data from a GraphQL server, or saving data to the cache or browser local storage. Handling these tasks within your actions or reducers can lead to a messy and less maintainable codebase. Redux Saga provides a solution for managing these side effects in a separate place, making your codebase cleaner and easier to maintain.
Basic example of using Redux Saga
To provide a clear example of how Redux Saga can be used, let’s consider a chat room application. When a user writes a message, we want to immediately display that message on the screen to provide prompt feedback. In Redux, we would define an action called addMessage
that would be dispatched when a new message is created. Similarly, we would have a reducer called messages
that would handle updating the state with the new messages.
To initialize Redux Saga in your application, you first need to import the createSagaMiddleware
function from the redux-saga
library. Then, create a Saga middleware and apply it to your Redux store. Finally, run the saga by passing it to the run
method of the middleware.
Here’s an example of the necessary steps to set up Redux Saga:
import createSagaMiddleware from 'redux-saga';
// Create the Saga middleware
const sagaMiddleware = createSagaMiddleware();
// Create the Redux store and apply the Saga middleware
const store = createStore(
reducers,
applyMiddleware(sagaMiddleware)
);
// Run the saga
import handleNewMessage from './sagas';
sagaMiddleware.run(handleNewMessage);
In this example, we assume that the handleNewMessage
saga is defined in a separate file called sagas.js
.
How Redux Saga works behind the scenes
Redux Saga is a middleware that intercepts Redux actions and injects its own functionality. To understand how it works, let’s cover some key concepts: saga, generator, middleware, promise, pause, resume, effect, dispatch, action, fulfilled, resolved, yield, and yielded.
A saga can be thought of as a “story” that reacts to an effect caused by your code. It contains tasks like making HTTP requests or performing other asynchronous operations. Sagas are implemented as generator functions, denoted by the function*
syntax.
To use Redux Saga, you create a middleware with a list of sagas to run, and then connect this middleware to your Redux store.
Here’s an example of a saga that sends a message to a WebSocket server every time the ADD_MESSAGE
action is dispatched:
import { takeEvery } from 'redux-saga/effects';
function* handleNewMessage(params) {
const socket = new WebSocket('ws://localhost:8989');
yield takeEvery('ADD_MESSAGE', (action) => {
socket.send(JSON.stringify(action));
});
}
export default handleNewMessage;
In this saga, the takeEvery
helper function is used, which waits for any ADD_MESSAGE
action to be dispatched and then executes the provided callback function.
The saga middleware suspends the execution of a saga at any yield
statement until the effect is fulfilled. Once the effect is fulfilled, the saga resumes execution until the next yield
statement is encountered. This allows for non-blocking behavior and better control over asynchronous tasks.
Basic Helpers
Redux Saga provides several helper functions that abstract away some of the low-level API calls. These helpers make it easier to write sagas and handle effects. Let’s briefly introduce some of the basic helpers:
takeEvery()
takeEvery()
is a helper function that allows you to run a saga every time a specific action is dispatched. It ensures that multiple instances of the saga can run concurrently, without waiting for the previous instances to complete.
Here’s an example using takeEvery()
:
import { takeEvery } from 'redux-saga/effects';
function* watchMessages() {
yield takeEvery('ADD_MESSAGE', postMessageToServer);
}
In this example, the watchMessages
generator pauses until an ADD_MESSAGE
action is dispatched. When the action occurs, it calls the postMessageToServer
function.
takeLatest()
takeLatest()
is similar to takeEvery()
, but it only allows one instance of the saga to run at a time. If a new action occurs while the saga is still running, it cancels the previous instance and starts executing with the latest data available.
take()
take()
is a helper function that waits for a specific action to be dispatched. Once the action is dispatched, the promise returned by take()
is resolved, and the saga can resume execution.
put()
put()
is a helper function used to dispatch an action to the Redux store. Instead of manually passing the Redux store or the dispatch action to the saga, you can simply use put()
.
Here’s an example:
yield put({ type: 'INCREMENT' });
yield put({ type: 'USER_FETCH_SUCCEEDED', data: data });
put()
returns a plain object that can be easily inspected and tested.
call()
call()
is a helper function that allows you to call a function in a saga. It wraps the function call in a way that makes it easier to test and inspect in your code.
Here’s an example:
yield call(delay, 1000);
In this example, call(delay, 1000)
returns an object that can be easily inspected or tested.
Running effects in parallel
Redux Saga provides two main helpers for running effects in parallel: all()
and race()
.
all()
You can use the all()
helper to run multiple effects in parallel. When you have multiple effects that need to be executed concurrently, you can wrap them in all()
.
Here’s an example:
import { all, call } from 'redux-saga/effects';
const [todos, user] = yield all([
call(fetch, '/api/todos'),
call(fetch, '/api/user')
]);
In this example, both fetch()
calls will execute concurrently, and the all()
effect won’t be resolved until both promises are fulfilled.
race()
race()
is another helper function provided by Redux Saga. Unlike all()
, race()
only waits for the first effect to complete, discarding the other effects. It’s commonly used to cancel a background task when a specific action occurs.
Here’s an example:
import { race, call, take } from 'redux-saga/effects';
function* someBackgroundTask() {
while(1) {
// ...
}
}
yield race({
bgTask: call(someBackgroundTask),
cancel: take('CANCEL_TASK')
});
In this example, the saga waits for either the background task to complete or for the CANCEL_TASK
action to be dispatched. If the action occurs, the background task is canceled.
That’s it for the introduction to Redux Saga! We covered when to use Redux Saga, a basic example of using it in a chat room app, and the basic helpers provided by Redux Saga. We also explained how Redux Saga works behind the scenes and introduced the all()
and race()
helpers for running effects in parallel.
Tags: Redux Saga, Redux Middleware, Side Effects, Reducers, Actions, Sagas, Generators, Promises