Handle side effects in react using redux-saga
Redux-Saga is a middleware library for Redux that makes side effects easier to manage and more efficient to execute. By using Redux-Saga, you can handle asynchronous operations, such as data fetching and impure interactions with the browser and external APIs, in a more organized and maintainable way.
Sagas are constructed using Generator functions, producing objects that are yielded to the redux-saga middleware. These yielded objects act as instructions for the middleware to interpret. Whenever a Promise is yielded to the middleware, the Saga is temporarily halted until the Promise is fulfilled.
More about generator functions here…
Setup
To get started with Redux-Saga, you’ll first need to install the package via npm or yarn:
npm install redux-saga
Next, you’ll need to create a new saga file where you’ll define your sagas. Let’s take a look at a basic example below:
import { call, put, takeEvery } from 'redux-saga/effects';
import { fetchDataSuccess, fetchDataError } from './actions';
import Api from './api';
function* fetchDataSaga(action) {
try {
const data = yield call(Api.fetchData, action.payload);
yield put(fetchDataSuccess(data));
} catch (error) {
yield put(fetchDataError(error));
}
}
function* main() {
yield takeEvery('FETCH_DATA_REQUESTED', fetchDataSaga);
}
export default rootSaga;
In this example, we define a fetchDataSaga
that handles fetching data from an API and dispatches success or error actions based on the result. We then create a rootSaga
that uses takeEvery
to watch for specific actions and trigger the corresponding saga.
Notice the
call
andput
keywords. these are called effects. We will discuss each of them in detail.
Create a root reducer
Let’s setup root saga so that all the saga are present in one file. It makes it easier to configure the middleware later. We will see how to do that in next steps.
import { all } from 'redux-saga/effects';
import main from './FetchDataSaga'; // import your saga
export default function* rootSaga() {
yield all([
main(), // use your saga
]);
}
Configure the middleware in redux-store
Assuming you already have a reducer and root reducer. You will need to create saga middleware, add it to middlwares and run the rootSaga
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = configureStore({
rootReducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(sagaMiddleware),
});
// then run the saga
sagaMiddleware.run(rootSaga)
Redux Saga Effects.
Below are the various effects provided by redux saga with the usage. we will also discuss the real world scenarios in the next section.
- call: This effect is used to call functions, promise-returning functions, or other sagas. It helps manage the flow of synchronous and asynchronous operations by waiting for the function or saga to complete before proceeding further.
import { call } from 'redux-saga/effects'; function* fetchData() { const result = yield call(api.fetchData); // Further logic after fetching data }
put: This effect dispatches actions to the Redux store. It allows sagas to trigger changes in the application state by dispatching actions, leading to the execution of corresponding reducers.
import { put } from 'redux-saga/effects'; function* exampleSaga() { yield put({ type: 'ACTION_TYPE', payload:{x:y} }); // Redux store will dispatch 'ACTION_TYPE' }
take: This effect listens for a specific action to be dispatched to the Redux store. When the specified action is detected, the saga resumes execution. It’s useful for handling asynchronous operations based on certain actions.
import { take } from 'redux-saga/effects'; function* watchAction() { yield take('WATCHED_ACTION'); // Saga resumes when 'WATCHED_ACTION' is dispatched }
takeEvery and takeLatest: These are helper functions that simplify the process of handling multiple occurrences of a specific action.
takeEvery
allows the saga to run each time the specified action is dispatched, whiletakeLatest
ensures that only the latest occurrence is processed, canceling any previous instances.import { takeEvery, takeLatest } from 'redux-saga/effects'; function* handleEvery() { yield takeEvery('WATCHED_ACTION', workerSaga); } function* handleLatest() { yield takeLatest('WATCHED_ACTION', workerSaga); }
all and race: These effects deal with handling multiple sagas concurrently.
all
is used to run multiple sagas concurrently and wait for all of them to complete, whilerace
is used to run multiple sagas concurrently but only wait for the first one to complete.import { all, race } from 'redux-saga/effects'; function* rootSaga() { yield all([saga1(), saga2()]); // or using race yield race({ task1: call(saga1), task2: call(saga2) }); }
fork and spawn: These effects are used to manage the concurrency of sagas.
fork
andspawn
both create a non-blocking fork, allowing the parent saga to continue executing without waiting for the forked saga to finish. The parent saga continues its execution regardless of the outcome of the forked task.spawn
is similar tofork
in that it creates a new task. However,fork
is used to create attached forks where asspawn
is used to create detached forks. Attached forks remain attached to their parent. that means, the parent saga will terminate only after all the attached forks are themselves terminated.import { fork, spawn } from 'redux-saga/effects'; function* parentSaga() { yield fork(childSaga); // or using spawn yield spawn(detachedSaga); }
delay: The
delay
effect is used to introduce a delay in the execution of the saga. It’s particularly useful for scenarios where you need to wait for a specific amount of time before proceeding with the next steps.import { delay } from 'redux-saga/effects'; function* delayedSaga() { yield delay(1000); // Waits for 1 second // Continue with the next steps }
select: This effect is used to access the current state of the Redux store. It allows sagas to retrieve specific pieces of information from the store, making it useful for scenarios where the saga’s behavior depends on the current state.
import { select } from 'redux-saga/effects'; function* getInfo() { const data = yield select(state => state.someData); // Use the selected data in the saga }
Real-World Scenarios
let’s dive into more realistic and detailed scenarios that you might encounter in app development:
User Authentication
Effects: take
, call
, and put
Imagine you’re building an app that requires user authentication. When a user attempts to log in, you need to make an API call to authenticate the user, then update your Redux store based on the result. Here’s how you might use Redux Saga to handle this
import { take, call, put } from 'redux-saga/effects';
import Api from '../api';
function* loginSaga() {
while (true) {
const { payload } = yield take('LOGIN_REQUEST');
try {
const user = yield call(Api.login, payload);
yield put({ type: 'LOGIN_SUCCESS', user });
} catch (error) {
yield put({ type: 'LOGIN_ERROR', error });
}
}
}
Data fetching from multiple sources.
Effect: all
Suppose your app needs to fetch multiple pieces of data at the same time, like a user’s profile and their posts. You can use all
to fetch this data in parallel
import { all, call, put } from 'redux-saga/effects';
import Api from '../api';
function* fetchDataSaga(userId) {
try {
const [profile, posts] = yield all([
call(Api.fetchProfile, userId),
call(Api.fetchPosts, userId)
]);
yield put({ type: 'FETCH_DATA_SUCCESS', profile, posts });
} catch (error) {
yield put({ type: 'FETCH_DATA_ERROR', error });
}
}
Non-Blocking Calls
Effect: fork
or spawn
In a chat application, you might want to allow the user to send messages while simultaneously fetching new messages. fork allows you to handle these tasks concurrently
import { fork, take, call, put } from 'redux-saga/effects';
import Api from '../api';
function* fetchMessagesSaga() {
while (true) {
yield take('FETCH_MESSAGES_REQUEST');
try {
const messages = yield call(Api.fetchMessages);
yield put({ type: 'FETCH_MESSAGES_SUCCESS', messages });
} catch (error) {
yield put({ type: 'FETCH_MESSAGES_ERROR', error });
}
}
}
function* sendMessageSaga() {
while (true) {
const { payload } = yield take('SEND_MESSAGE_REQUEST');
try {
yield call(Api.sendMessage, payload);
yield put({ type: 'SEND_MESSAGE_SUCCESS' });
} catch (error) {
yield put({ type: 'SEND_MESSAGE_ERROR', error });
}
}
}
function* chatSaga() {
yield fork(fetchMessagesSaga);
yield fork(sendMessageSaga);
}
Handling Timeouts
Effect: race
If your app makes API calls, you might want to handle scenarios where the API takes too long to respond. You can use race
to timeout the API call if it takes too long.
import { race, call, put, delay } from 'redux-saga/effects';
import Api from '../api';
function* fetchWithTimeoutSaga() {
const { response, timeout } = yield race({
response: call(Api.fetchData),
timeout: delay(5000)
});
if (response) {
yield put({ type: 'FETCH_SUCCESS', payload: response });
} else if (timeout) {
yield put({ type: 'FETCH_TIMEOUT' });
}
}