What is redux-saga?
redux-saga is a library that aims to make application side effects (i.e., asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage, more efficient to execute, and better at handling failures. It uses an ES6 feature called Generators to make those asynchronous flows easy to read, write, and test.
What are redux-saga's main functionalities?
Handling Asynchronous Actions
This feature allows you to handle asynchronous actions in a more readable and maintainable way. The code sample demonstrates how to fetch user data asynchronously using the `call` effect to call the API and `put` effect to dispatch actions.
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: 'USER_FETCH_SUCCEEDED', user: user});
} catch (e) {
yield put({type: 'USER_FETCH_FAILED', message: e.message});
}
}
function* mySaga() {
yield takeEvery('USER_FETCH_REQUESTED', fetchUser);
}
Managing Side Effects
redux-saga helps manage side effects like delays, API calls, and more. The code sample shows how to delay an increment action by 1 second using the `delay` effect.
import { delay } from 'redux-saga/effects';
function* incrementAsync() {
yield delay(1000);
yield put({ type: 'INCREMENT' });
}
function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync);
}
Handling Concurrency
redux-saga provides tools to handle concurrency, ensuring that only the latest action is processed. The code sample demonstrates using `takeLatest` to handle only the most recent fetch request.
import { takeLatest } from 'redux-saga/effects';
function* fetchData(action) {
try {
const data = yield call(Api.fetchData, action.payload);
yield put({ type: 'FETCH_SUCCEEDED', data });
} catch (error) {
yield put({ type: 'FETCH_FAILED', error });
}
}
function* mySaga() {
yield takeLatest('FETCH_REQUESTED', fetchData);
}
Other packages similar to redux-saga
redux-thunk
redux-thunk is a middleware that allows you to write action creators that return a function instead of an action. It is simpler and more lightweight compared to redux-saga, but it doesn't offer the same level of control over complex asynchronous flows.
redux-observable
redux-observable is an RxJS-based middleware for Redux that allows you to work with async actions using Observables. It is more powerful and flexible than redux-saga for handling complex async logic, but it has a steeper learning curve due to its reliance on RxJS.
rematch
rematch is a Redux framework that abstracts away much of the boilerplate associated with Redux. It includes built-in support for side effects and async actions, making it easier to use than redux-saga, but it may not offer the same level of customization and control.
redux-saga
Exploration of an alternative side effect model for Redux applications.
Instead of dispatching thunks which get handled by the redux-thunk middleware. You create Sagas
(not sure if the term applies correctly)
A Saga is a generator function that takes (getState, action)
and can yield side effects as well as
other actions.
Example
function* checkout(getState) {
const cart = getState().cart
try {
const cart1 = yield [api.buyProducts, cart]
yield actions.checkoutSuccess(cart1)
} catch(error) {
yield actions.checkoutFailure(error)
}
}
function* getAllProducts() {
const products = yield () => api.getProducts()
yield actions.receiveProducts(products)
}
export default function* rootSaga(getState, action) {
switch (action.type) {
case types.GET_ALL_PRODUCTS:
yield* getAllProducts(getState)
break
case types.CHECKOUT_REQUEST:
yield* checkout(getState)
}
}
plug redux-saga in the middleware pipeline
const createStoreWithSaga = applyMiddleware(
sagaMiddleware(rootSaga)
)(createStore)
export default function configureStore(initialState) {
return createStoreWithSaga(reducer, initialState)
}
The difference from redux-thunk, is that the decision of what to dispatch is not scattered throughout
the action creators, but instead centralized in one place that is an integrated part of you domain logic.
How does it work
-
No application logic inside action creators. All action creators are pure factories of raw-data actions
-
All the actions hit the reducers; even "asynchronous" ones. All actions hit also the Saga.
-
Reducers are responsible of transitioning state between actions
-
Sagas are responsible of orchestrating operations (side effects or actions)
-
Sagas are generator functions that can yield
- a thunk of the side effet (e.g.
yield () => api.buyProducts(cart)
) - an array
[fn, ...args]
(e.g. yield () => [api.buyProducts, cart]
) - a dispatch item which will get dispatched and handled by a dedicated middleware (e.g.
yield {[API_CALL]: { endpoint: 'getProducts', payload: [cart] }}
)
Sagas don't execute side effects themselves, they create the intended side effect.
Then the side effect gets executed later by the appropriate service (either a middleware or a simple function).
Services return Promise to denote their future results.
The saga middleware takes the service response and resumes the Saga generator with the resolved response. This way
Sagas can describe complex workflows with a simple synchronous style. And since they are side-effect free, they can
be tested simply by driving the generator function and testing the successive results.
You can get the response returned from services inside your Saga, and use it
to yield further side effects or other actions. If the service responds with a rejected
promise, an exception is thrown inside the generator and can be handled by a normal
try/catch
block.
Here the Saga code from the Shopping cart example. Note that Sagas compose using the yield *
operator.
setup and run the examples
npm install
There are 2 examples ported from the Redux repos. You can observe the logged actions/effects
into the console (logged via the redux-logger middleware).
Counter example
npm run build-counter
Shopping Cart example
npm run build-shop
There are also specs samples that test the Saga generators
npm run test-counter
npm run test-shop