SAGA,
redux-saga
by Elizaveta Anatskaya
Redux is synchronous
Redux manages your application state synchronously.
One of the main concepts of Redux is reducers.
The reducer is a pure function that takes the previous state and an action, and returns the next state.
It’s very important that the reducer stays pure. Things you should never do inside a reducer:
- Mutate its arguments;
- Perform side effects like API calls and routing transitions;
- Call non-pure functions, e.g. Date.now() or Math.random().
Why's Saga?
middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer
Middleware to the rescue!
Middlewares don’t come out of the box with redux. It is usually a package that we will install, or we can write one for our selves.
“Sagas are implemented as generator functions that yield objects to the redux-saga middleware.”
— is a library that aims to make application side effects easier to manage (i.e. asynchronous things like data fetching and impure things like accessing the browser cache)
Redux-Saga
— is a redux middleware, which means this thread can be started, paused and cancelled from the main application with normal redux actions, it has access to the full redux application state and it can dispatch redux actions as well.
export function* helloSaga() {
console.log('Hello Sagas!')
}
- A generator function is defined by the function* declaration
- Generator functions can be exited and later re-entered. Their context (variable bindings) will be saved across re-entrances.
- Generator function returns a Generator object, which is a kind of Iterator.
So what are generators?
//define
function* generator(i) {
yield i;
yield i + 10;
}
//init
const gen = generator(10);
//use
console.log(gen.next()); // {value: 10, done: false}
console.log(gen.next()); // {value: 20, done: false}
console.log(gen.next()); // {value: undefined, done: true}
- Calling next() on the first time executes the first yield , which returns {value: 10, done: false}. done is false since the generator is not done yet.
- Calling next() the second time, we return to the generator function in the exact place we left it and execute the function until we reach the second yield . The second yield returns {value: 20, done: true} since i + 10 is equal to 20, and the generator is not done yet.
- Calling next() the third time will return {value: undefined, done: true} since the generator has already yielded its last value.
So what are generators?
// define
function* generator() {
const i = 1000;
yield i;
const result = yield i + 1;
yield i + result;
}
// init
const gen = generator();
// use
console.log(gen.next()); // {value: 1000, done: false}
console.log(gen.next()); // {value: 1001, done: false}
console.log(gen.next(3)); // {value: 1003, done: false}
console.log(gen.next()); // {value: undefined, done: true}
👈 Wait. What?? 🤔
Here is why: calling the next() method with an argument will resume the generator function execution, replacing the yield expression where execution paused with the argument from next().
On the third time, we call next() the returned object is {value: 1003, done: false} because i equals to 1000 and then we pass the value 3 to the next method, hence 1000 + 3 yielded 1003.
...so now that we’ve learned how to use Generator, we are ready to learn about Redux-Saga! 💪
Redux-Saga setup
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import reducer from './reducers';
import rootSaga from './saga';
// create the saga middleware
export const sagaMiddleware = createSagaMiddleware();
// mount it on the store
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
);
// run the saga
sagaMiddleware.run(rootSaga);
When we create the Redux store, we also create our Redux-Saga middleware and connect it to the store via applyMiddleware. After the store was created, we call run with our root saga, which starts our redux-saga middleware.
Watchers and Workers
// watcher saga
export function* watchGetItems() {
yield takeEvery('GET_ITEMS_REQUEST_ACTION', getItems);
}
// worker saga
export function* getItems() {
const items = yield call(api.getItems);
yield put({
type: 'GET_ITEMS_SUCCESS_ACTION',
payload: items
});
}
- The main saga file is usually split into two different types of sagas: Watchers and workers
- Watcher saga sees every action that is dispatched to the redux store; if it matches the action it is told to handle, it will assign it to its worker saga
- The worker saga is running all the side effects it was meant to do
- The watcher saga is typically the root saga to export and mount on the store
Watchers and Workers
// watcher saga
export function* watchGetItems() {
yield takeEvery('GET_ITEMS_REQUEST_ACTION', getItems);
}
// worker saga
export function* getItems() {
const items = yield call(api.getItems);
yield put({
type: 'GET_ITEMS_SUCCESS_ACTION',
payload: items
});
}
In the above example, the watcher saga is listening to GET_ITEMS_REQUEST_ACTION. When this action type is dispatched, the watcher calls getItems saga. Then, Redux-Saga will start executing getItems function. On the first yield, we call some API, and on the second yield, we dispatch another action of type GET_ITEMS_SUCCESS_ACTION and payload containing the result of the previous yield.
call and put are effect creators.
Effect Creators
- select: returns the full state of the application
- put: dispatch an action into the store (non-blocking)
- call: run a method, Promise or other Saga (blocking)
- take: wait for a redux action/actions to be dispatched into the store (blocking)
-
cancel: cancels the saga execution.
- fork: performs a non-blocking call to a generator or a function that returns a promise. It is useful to call fork on each of the sagas you want to run when you start your application since it will run all the sagas concurrently. (non-blocking)
- debounce: the purpose of debounce is to prevent calling saga until the actions are settled off. Meaning, until the actions we listen on will not be dispatched for a given period. For example, dispatching autocomplete action will be processed only after 100 ms passed from when the user stopped typing.
- throttle: the purpose of throttle is to ignore incoming actions for a given period while processing a task. For example, dispatching autocomplete action will be processed every 100 ms, while the processed action will be the last dispatched action in that period. It will help us to ensure that the user won’t flood our server with requests.
- delay: block execution for a predefined number of milliseconds.
are redux actions which serve as instructions for Saga middleware
Helpers
- takeEvery: takes every matching action and run the given saga (non-blocking)
export function* watcherSaga() {
yield takeEvery('SOME_ACTION', workerSaga);
}
- takeLatest: takes every matching action and run the given saga, but cancels every previous saga that is still running (blocking)
export function* watcherSaga() {
yield takeLatest('SOME_ACTION', workerSaga);
}
Practice
Well done!
Title Text
Saga
By Elizabeth Anatskaya
Saga
- 265