ref –
- https://blog.logrocket.com/understanding-redux-saga-action-creators-sagas/
- https://blog.devgenius.io/reactjs-simple-understanding-redux-with-redux-saga-f635e273e24a
- https://dog.ceo/dog-api/
- https://redux-saga.js.org/
Bare Redux won’t give us much flexibility. At its core, Redux is only a state container that supports synchronous data flows: every time an action is sent to the store, a reducer is called and the state is updated immediately.
But in an asynchronous flow, you have to wait for the response first;
then, if there’s no error, you can update the state.
And what if your application has a complex logic/workflow?
Redux uses middleware to solve this problem
A middleware is a piece of code that is executed after an action is dispatched, but before it reaches the reducer.
Its core function is to:
1) intercept the action sent to the reducer,
2) perform any asynchronous operation that may be present in the action,
3) and present an object to the reducer.
Redux Saga does this with the help of ES2015 generators
1 2 3 4 5 |
function* myGenerator(){ let first = yield 'first yield value'; let second = yield 'second yield value'; return 'third returned value'; } |
What are generator functions
Generators are functions that can be paused during execution and resumed, instead of executing all of a function’s statements in one pass.
When you invoke a generator function, it will return an iterator object. With each call of the iterator’s next() method, the generator’s body will be executed until the next yield statement, where it will then pause:
1 2 3 4 5 |
const it = myGenerator(); console.log(it.next()); // {value: "first yield value", done: false} console.log(it.next()); // {value: "second yield value", done: false} console.log(it.next()); // {value: "third returned value", done: true} console.log(it.next()); // {value: "undefined", done: true} |
This can make asynchronous code easy to write and understand. For example, instead of doing this:
1 2 |
const data = await fetch(url); console.log(data); |
With generators, we can do this:
1 2 |
let val = yield fetch(url); console.log(val); |
And with Redux Saga, we generally have a saga whose job is to watch for dispatched actions:
1 2 3 |
function* watchRequestDog() { } |
To coordinate the logic we want to implement inside the saga, we can use a helper function like takeEvery to spawn a new saga to perform an operation:
1 2 3 4 5 6 7 8 9 |
// Watcher saga for distributing new tasks function* watchRequestDog() { yield takeEvery('FETCHED_DOG', fetchDogAsync) } // Worker saga that performs the task function* fetchDogAsync(){ } |
Using Redux Saga to handle multiple async requests
If there are multiple requests, takeEvery will start multiple instances of the worker saga; in other words, it handles concurrency for you.
Recalling our example, we could implement the fetchDogAsync() function with something like this (assuming we had access to the dispatch method):
1 2 3 4 5 6 7 8 9 |
function* fetchDogAsync(){ try{ yield dispatch(requestDog()); const data = yield fetch(...); yield dispatch(requestDogSuccess(data)); }catch (error){ yield dispatch(requestDogError()); } } |
by using yield and dispatch, we can have saga dispatch actions to reducers and do async operations (such as fetches) all in the order we want.
1) dispatch action requestDog
2) fetch dog data
3) with the returned data, we dispatch it to the reducer to save
Call
Just as in Redux you use action creators to create a plain object describing the action that will get executed by the Store, call creates a plain object describing the function call. The redux-saga middleware takes care of executing the function call and resuming the generator with the resolved response.
Redux Saga allows us to yield an object that declares our intention to perform an operation,
rather than yielding the result of the executed operation itself.
In other words, the above example is implemented in Redux Saga in this way:
1 2 3 4 5 6 7 8 9 |
function* fetchDogAsync(){ try{ yield put(requestDog()) const data = yield call(() => fetch(...)) yield put(requestDogSuccess(data)) }catch(error){ yield put(requestDogError()) } } |
Instead of invoking the asynchronous request directly, the method call will return only a plain object describing the operation like so:
1 |
yield call(() => fetch(...)) |
Redux Saga then takes care of the invocation and return the result to the generator.
Put
The same thing happens with the put method. Instead of dispatching an action inside the generator, put returns an object with instructions for the middleware to dispatch the action.
Those returned objects are called effects.
Here’s an example of the effect returned by the call method:
1 2 3 4 5 6 |
{ CALL: { fn: () => {/* ... */}, args: [] } } |
Call vs Put
call function accepts rest arguments, which will be passed to api.fetchUser
function.
Instructing middleware to call promise, it resolved value will be assigned to userData
variable
1 2 |
function* fetchUserSaga(action) { const userData = yield call(api.fetchUser, action.userId); |
Instructing middleware to dispatch corresponding action.
1 2 3 4 5 |
yield put({ type: 'FETCH_USER_SUCCESS', userData, }); } |
Composing Effects
We can very easily compose many effects into a complex workflow.
In addition to takeEvery, call, and put, Redux Saga offers a lot of effect creators for:
– throttling
– getting the current state
– running tasks in parallel
– cancel tasks, etc
Running Tasks In Parallel
The yield statement is great for representing asynchronous control flow in a linear style, but we also need to do things in parallel. We can’t write:
1 2 3 |
// wrong, effects will be executed in sequence const users = yield call(fetch, '/users') const repos = yield call(fetch, '/repos') |
Because the 2nd effect will not get executed until the first call resolves. Instead we have to write:
1 2 3 4 5 6 7 |
import { all, call } from 'redux-saga/effects' // correct, effects will get executed in parallel const [users, repos] = yield all([ call(fetch, '/users'), call(fetch, '/repos') ]) |