The Complete Guide to Redux Saga With Real life example
Managing Side Effects in Your Redux App
Introduction
Redux Saga is like a battlefield commander coordinating complex missions behind the scenes. Just like a commander carefully manages troop logistics and strategies, Redux Saga orchestrates asynchronous flows and side effects outside of your React components.
Without Saga, components become clogged with tangled asynchronous logic and business rules. As an app grows, this makes it harder to understand, maintain, and extend. Redux Saga provides the missing coordination layer to scale apps gracefully
1. What is Redux Saga?
Redux Saga is a middleware library for managing side effects in your Redux application. Side effects refer to anything that affects state change in your app outside of standard actions - things like asynchronous API calls, web socket communications, and accessing browser data and APIs.
Redux Saga provides an easier way to handle all of these complex workflows right inside your Redux store. It uses an ES6 feature called generators to make asynchronous code look synchronous.
The main benefits of Redux Saga include:
Making side effects like data fetching and impure functions easier to manage
Simple control flow for complex asynchronous operations
Better separation of business logic from component code
Easier testing using sagas as pure functions
Overall, Redux Saga aims to help you better organize your Redux applications by moving side effects outside your components and into separate sagas. This makes your code more maintainable and scalable as your app grows.
2. Setting Up Redux Saga
The Redux Saga library can be installed via NPM or Yarn:
npm install redux-saga
or
yarn add redux-saga
To integrate Redux Saga into your application, you will need to:
Create a saga middleware
Connect the saga middleware to the Redux store
Import and run sagas from the root saga
Attach the saga middleware to the store
Here is an example setup with a simple root saga:
// saga.js
import { takeEvery } from 'redux-saga/effects';
function* watchFetchDataSaga() {
yield takeEvery('FETCH_DATA', fetchDataSaga);
}
export default function* rootSaga() {
yield all([
watchFetchDataSaga()
]);
}
// configureStore.js
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga);
export default store;
This hooks up the Redux Saga middleware to listen for dispatched actions and run our sagas.
3.Basic Concepts of Redux Saga
Some key concepts to understand when working with Redux Saga:
Sagas: Generator functions that are triggered by Redux actions and can also dispatch Redux actions. Used for managing side effects.
Effects: Helper functions provided by Redux Saga to call external APIs, dispatch actions, and more.
Tasks: Units of execution scheduled by a saga using effects like take
and fork
.
Middleware: Redux Saga middleware is created using createSagaMiddleware
and connect to the Redux store. This allows sagas to listen and respond to dispatched actions.
A basic saga workflow goes as follows:
Actions are dispatched to the Redux store
Saga middleware triggers matching sagas to run
Sagas execute effects like API calls, dispatching actions
Actions are dispatched by sagas back to reducers
Store state updates accordingly
To demonstrate the core concepts, let's look at a simple user login flow:
Sagas - loginSaga
handles the full login process. It dispatches actions to components and invokes APIs.
Effects - call
is used to call the API login function. put
dispatches actions.
Tasks - validateUser
could fork a task to validate the user in the background.
// sagas.js
function* loginSaga(action) {
try {
// call API
const user = yield call(api.login, action.credentials);
// dispatch success action
yield put({type: 'LOGIN_SUCCESS', user});
} catch (err) {
yield put({type: 'LOGIN_ERROR', err});
}
}
function* validateUser(user) {
// fork validation task
// dispatches actions on complete
}
This shows how sagas coordinate the overall flow while effects handle individual steps.
This allows for keeping business logic separated from components and managing complex workflows in sagas.
4. Understanding Generator Functions
Generator functions provide a powerful capability in JavaScript - the ability to pause and resume function execution. This enables writing asynchronous, non-blocking code in a simple synchronous style.
Let's look at how they work:
When a regular function is called, it runs straight through and returns a value:
function fetchUser() {
// do work
return user
}
const user = fetchUser()
But generator functions are different. When called, they return a generator object without running any code:
function* fetchUserSaga() {
// function body
}
const genObj = fetchUserSaga() // no code executed yet
To execute the generator, you call next()
on the generator object. This runs the code until the first yield
expression:
genObj.next()
// Runs until first yield:
function* fetchUserSaga() {
// start execution
yield put(actions.requestStart())
// execution pauses here on yield
}
The yield
keyword pauses the function execution and returns control to the caller of next()
. It also returns the value following it (like put()
here) out to the caller.
Later, calling next()
again resumes where it left off until the next yield
:
genObj.next() // dispatch requestStart
genObj.next() // resume execution
// Runs next lines:
function* fetchUserSaga() {
// already executed
const user = yield call(Api.fetchUser) // pauses again
// will run after next next() call
}
This start/stop ability enables writing asynchronous flows in a synchronous-looking style. The generator manages state between yields.
Redux Saga uses this heavily, yielding effects like put
and call
to orchestrate complex async workflows. So generators power the synchronous coding style for sagas.
Here is a complete example demonstrating generator functions and how Redux Saga utilizes them:
// Generator function
function* fetchUserSaga() {
// Yield effects like put() and call()
// to pause execution
console.log('Starting saga...')
// Pause, dispatch action
yield put({type: 'FETCH_USER_STARTED'})
// Pause again, wait for API call
const user = yield call(fetchUserFromAPI)
// Resume, dispatch success action
yield put({type: 'FETCH_USER_SUCCEEDED', user})
console.log('Finishing saga...')
}
// Calling the generator function
// returns a generator object
const sagaGenObj = fetchUserSaga()
// Run the generator by calling next()
sagaGenObj.next() // Logs "Starting saga..."
// Next call runs until first yield
sagaGenObj.next() // Dispatches "FETCH_USER_STARTED"
// Simulate asynchronous operation
setTimeout(() => {
// Resume where previous yield left off
sagaGenObj.next(mockUserData)
// Runs until next yield
sagaGenObj.next() // Dispatches "FETCH_USER_SUCCEEDED"
console.log('Saga complete!')
}, 2000)
In this example, we see how the generator function fetchUserSaga
can be paused and resumed based on yielding effects and calling next()
.
The core ideas are:
Generator functions allow starting and stopping execution
The
yield
keyword returns data and pauses executionCalling
next()
resumes execution until the nextyield
This enables writing async flows in a synchronous style
Redux Saga uses these generator capabilities to orchestrate complex async logic for side effects. The saga middleware handles running the generators and effects behind the scenes.
5. Working with Effects
Redux Saga provides various effects that make managing side effects easier. Some common effects include:
take
- Pauses saga execution until specified action is dispatched
put
- Dispatches action to the Redux store
call
- Calls a function, returns result
fork
- Forks a separate task in the background
select
- Selects state from the Redux store
Let's check with example :
take
- When take
is yielded, the saga middleware will pause execution and subscribe to the provided action type. When that action is dispatched, take
will resume and return the action object.
// Wait for action
const {payload} = yield take('FETCH_PRODUCTS')
take.maybe
- Same as take
but does not pause execution. Can be used to conditionally run logic on an action.
put
- When put
is yielded, the saga middleware will dispatch the provided action object to the Redux store.
yield put({type: 'ADD_PRODUCT', payload})
call
- When call
is yielded, the saga will call the provided function and pause until it returns. It returns the result to the saga.
const data = yield call(fetchAPI)
apply
- Similar to call
but used for calling a function and passing arguments from the saga.
fork
- When fork
is yielded, the saga will spawn a new task that runs in the background without blocking execution.
yield fork(handleAsyncLogic) // runs in parallel
join
- Blocks execution until a forked task finishes. Joins on the provided task identifier.
race
- Runs multiple effects concurrently and resumes on the first to finish.
So in summary, effects like take
and call
pause saga execution while put
, fork
schedule work to occur. This provides a powerful declarative API for orchestrating async flows and coordinating actions.
Let me know if you would like any clarification or have additional questions!
6. Handling Asynchronous Operations
Here is a more in-depth explanation of handling asynchronous operations with Redux Saga:
Managing asynchronous flows is where Redux Saga shines. With generator functions and effects, Sagas enable coordinating async logic in a synchronous-looking style.
For example, here is a saga that fetches user data from an API:
// Fetches user data asynchronously
function* fetchUserSaga(action) {
try {
// Indicate pending state
yield put({type: 'FETCH_USER_PENDING'})
// Call API using call effect
const user = yield call(api.fetchUser, action.userId)
// Dispatch success action
yield put({type: 'FETCH_USER_FULFILLED', payload: user})
} catch (err) {
// Dispatch failure action
yield put({type: 'FETCH_USER_REJECTED', error: err})
}
}
Here is what happens step-by-step:
The saga is triggered by a
FETCH_USER
action containing theuserId
to fetchIt first dispatches a pending action to update state
It then calls the async API function using the
call
effectOn success, it dispatches a success action with the user data
On failure, it dispatches a failure action containing the error
This allows the component dispatching the action to stay in sync with the async request status. It's kept completely separate from how the API call and data fetching is orchestrated.
Sagas can also watch actions using take
effects:
// Watches for FETCH_USER action
function* watchFetchUser() {
while(true) {
const action = yield take('FETCH_USER')
yield fetchUserSaga(action)
}
}
This allows sagas to orchestrate async flows in response to dispatched Redux actions. By using effects like call
, put
and take
, complex async workflows can be managed elegantly and purely.
7. Combining Multiple Sagas
For any real application, you'll need to organize and combine multiple sagas.
Managing asynchronous flows is where Redux Saga shines. With generator functions and effects, Sagas enable coordinating async logic in a synchronous-looking style.
For example, here is a saga that fetches user data from an API:
// Fetches user data asynchronously
function* fetchUserSaga(action) {
try {
// Indicate pending state
yield put({type: 'FETCH_USER_PENDING'})
// Call API using call effect
const user = yield call(api.fetchUser, action.userId)
// Dispatch success action
yield put({type: 'FETCH_USER_FULFILLED', payload: user})
} catch (err) {
// Dispatch failure action
yield put({type: 'FETCH_USER_REJECTED', error: err})
}
}
Here is what happens step-by-step:
The saga is triggered by a
FETCH_USER
action containing theuserId
to fetchIt first dispatches a pending action to update state
It then calls the async API function using the
call
effectOn success, it dispatches a success action with the user data
On failure, it dispatches a failure action containing the error
This allows the component dispatching the action to stay in sync with the async request status. It's kept completely separate from how the API call and data fetching is orchestrated.
Sagas can also watch actions using take
effects:
// Watches for FETCH_USER action
function* watchFetchUser() {
while(true) {
const action = yield take('FETCH_USER')
yield fetchUserSaga(action)
}
}
This allows sagas to orchestrate async flows in response to dispatched Redux actions. By using effects like call
, put
and take
, complex async workflows can be managed elegantly and purely.
Here are some additional details on combining multiple sagas in a modular way:
Splitting sagas into multiple files helps organize different domains, features, and workflows in your application. Some common ways to split them are:
By route or page
By entity like users, products, cart
By workflow like auth, checkout, payments
By feature like analytics, messaging, notifications
For example, the auth sagas may include:
authSagas.js
import { call, put } from 'redux-saga/effects'
// Log in saga
export function* logInSaga(action) {
//...
}
// Sign up saga
export function* signUpSaga(action) {
// ...
}
These auth-related sagas can be combined into a rootSaga
:
rootSaga.js
import { all } from 'redux-saga/effects'
import * as authSagas from './authSagas'
export default function* rootSaga() {
yield all([
authSagas.logInSaga(),
authSagas.signUpSaga()
// other sagas
])
}
The all
effect runs the provided sagas concurrently. This allows independent workflows to progress simultaneously.
Other useful effects for combining sagas include:
race
- Runs sagas concurrently, cancel others on completionjoin
- Blocks execution until specified sagas completefork
- Spawns sagas in background without blocking
Properly splitting sagas improves code organization, isolates concerns, and can help performance by running tasks concurrently when possible.
8.Since sagas are generator functions, try/catch blocks can be used to handle errors:
jsCopy codefunction* fetchUserSaga(action) {
try {
// Call API
const user = yield call(api.fetchUser, action.userId)
// Dispatch success action
yield put(actions.fetchSuccess(user))
} catch (error) {
// Dispatch failure action
yield put(actions.fetchFailure(error))
}
}
The call
effect returns normal values or throws exceptions just like a regular function. So surrounding API calls with try/catch allows catching failures.
For API errors, the saga can dispatch appropriate actions to update state. For other errors like programming errors, a saga could log to the console before aborting.
Testing Sagas
Sagas lend themselves well to testing since they are pure functions with no side effects inside the saga. The redux-saga-test-plan
library provides a simple API for asserting effects:
jsCopy codeconst fetchUser = testSaga(fetchUserSaga)
fetchUser
.put(actions.requestStart())
.call(api.fetchUser, userId)
.put(actions.requestSuccess(user))
This validates the saga yields the expected effects in order. Any errors will fail the assertion.
So sagas enable:
Catching errors within sagas using try/catch
Easily testing sagas since they are pure functions
Isolating side effects from components
Proper error handling and testing ensures sagas remain robust and reliable for orchestrating complex flows.
9. Real-World Use Cases
There are many scenarios where Redux Saga simplifies complex flows:
Data Fetching: Fetching data from multiple sources, handling loading and error states
Authentication: Log in/out flows, redirection on auth events, account verification
Real-time: Web socket communication, live data streams
API Request Management: Caching, retries, polling, request deduplication
Analytics: Tracking user events, page views, performance data
Asynchronous UX: Auto-saving, notifications, location-based triggers
These types of complex workflows with multiple moving parts are perfect for sagas.
Here is an example real world codebase using Redux Saga for state management and side effects:
E-Commerce App with Redux Saga
This example implements common e-commerce workflows like authentication, product browsing, cart management, and checkout in a React + Redux app using Redux Saga for side effect handling.
Project Structure
src/
|- components/ - presentational React components
|- redux/
|- slices/ - Slice reducers and actions
|- sagas/ - Redux Saga files
|- store.js - Redux store setup
|- App.js - Root component
|- index.js - Entry point
Key files:
store.js:
// Create saga middleware
const sagaMiddleware = createSagaMiddleware()
// Mount saga middleware on store
const store = configureStore({
reducer: rootReducer,
middleware: [sagaMiddleware]
})
// Run saga monitor for logging
sagaMonitor.setEnabled()
// Run root saga
sagaMiddleware.run(rootSaga)
export default store
rootSaga.js
// Root saga combines all feature sagas
export default function* rootSaga() {
yield all([
authSagas(),
productsSagas(),
cartSagas(),
ordersSagas()
])
}
Authentication Sagas
The auth sagas handle login, logout and user session handling.
sagas/auth.js
import { call, put, take } from 'redux-saga/effects'
import { loginSuccess, loginFailed } from '../slices/auth'
// Worker saga - log user in
export function* loginSaga(action) {
try {
// Call API endpoint
const user = yield call(api.login, action.payload)
// Dispatch success action
yield put(loginSuccess(user))
} catch (error) {
// Dispatch failure action
yield put(loginFailed(error))
}
}
// Watcher saga - watch for login request
export function* watchLoginSaga() {
while (true) {
const action = yield take('LOGIN_REQUEST')
yield loginSaga(action)
}
}
The watchLoginSaga
waits for the LOGIN_REQUEST
action and triggers the worker saga. The worker handles calling the API and dispatching appropriate actions on success/failure.
Logout and session handling follows a similar pattern.
Products Sagas
These sagas handle product data fetching.
sagas/products.js
import { call, put } from 'redux-saga/effects'
import { fetchProductsSuccess } from '../slices/products'
// Fetch products from API
export function* fetchProductsSaga() {
try {
const products = yield call(api.fetchProducts)
yield put(fetchProductsSuccess(products))
} catch (error) {
// Handle error
}
}
export function* watchFetchProducts() {
yield take('FETCH_PRODUCTS')
yield fetchProductsSaga()
}
This pattern fetches products when the FETCH_PRODUCTS
action is dispatched. The products slice handles updating the state.
Cart Sagas
Manages cart actions like adding/removing items and checkout.
sagas/cart.js
import { take, call, put, select } from 'redux-saga/effects'
import { getCart } from '../slices/cart'
// Add item to cart
export function* addToCartSaga(action) {
// Get current cart from state
const state = yield select(getCart)
const itemExists = state.items.find(...)
if (itemExists) {
// Increment quantity
} else {
// Add new item
}
// Call API to update cart
yield call(api.updateCart, updatedCart)
// Dispatch cart updated action
yield put(cartUpdated(updatedCart))
}
// Watch for add to cart action
export function* watchAddToCart() {
yield take('ADD_TO_CART')
yield addToCartSaga()
}
The cart sagas demonstrate selecting state, calling APIs, and dispatching actions to coordinate cart workflows.
Summary
Some key points:
Sagas are split by domain for organization
Watcher sagas respond to dispatched actions
Worker sagas handle business logic and side effects
Effects like call, put, take simplify async logic and flows
Error handling with try/catch blocks
Components stay focused on UI
This structure keeps components simple and business logic separate. Scaling and modifying workflows is easy since sagas are modular.
Redux Saga enables managing complexity in Redux through side effect handling and workflow orchestration.
10. Best Practices and Tips
Here are some tips for using Redux Saga effectively:
Keep sagas focused on a single task or domain
Favor small generator functions over large complex sagas
Use descriptive saga names like
watchAuthSaga
Prefer declarative effects like
take
andput
over imperative codeMinimize branching and conditionals in sagas
Use
takeLatest
for actions that should cancel previous callsLeverage selector functions for extracting data from state
Use sagas for orchestration, not calculation
Following best practices will help keep your sagas easy to test, understand and maintain.
11. Conclusion
Redux Saga provides a powerful abstraction for managing complex workflows in Redux apps. With generator functions and effects, it makes asynchronous flows feel simple and synchronous.
In this guide, we covered core concepts, practical examples, and real-world use cases for leveraging Redux Saga. From complex data fetching to long-running processes, Redux Saga can help you handle the most challenging side effects with ease.
By embracing sagas, you can build more resilient, testable and flexible applications on top of Redux. The community around Redux Saga is also actively growing with many addons and integrations available.
I encourage you to give Redux Saga a try in your next Redux app. Let me know if you have any other topics you'd like to see covered around Redux Saga!