ref – https://medium.com/@ravindermahajan/why-use-redux-saga-f3413a3f7e34
https://redux-saga.js.org/docs/introduction/BeginnerTutorial/
https://redux-saga.js.org/docs/advanced/RacingEffects/
https://redux-saga.js.org/docs/advanced/RunningTasksInParallel/
Reducers.js
First, we have reducers, which takes in actions, evaluate its type and return an updated state.
Reducers are executed whenever an action object is returned, or store.dispatch({…}) is executed
1 2 3 4 5 6 7 8 9 10 11 |
export default function counter(state = 0, action) { console.log(`reducer.js - evaluating action ${action.type}`) switch (action.type) { case 'INCREMENT': return state + 1 case 'DECREMENT': return state - 1 default: return state } } |
Component
Next we create an component. The component takes in four props. The current store value, an increment function, a decrement button, and async increment button. The component’s JSX has three buttons, each of which onclick will execute a passed in prop. Then it simply displays the store’s value.
Then let’s create a component Counter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import React from 'react' import { PropTypes } from 'prop-types'; console.log('declare Counter.js') const Counter = ({ value, onIncrement, onDecrement }) => <div> <button onClick={onIncrement}> execute action storeDispatch('INCREMENT') </button> {' '} <button onClick={onDecrement}> execute action storeDispatch('DECREMENT') </button> <hr /> <div> Clicked: {value} times </div> </div> Counter.propTypes = { value: PropTypes.number.isRequired, onIncrement: PropTypes.func.isRequired, onDecrement: PropTypes.func.isRequired } export default Counter |
Main
Now let’s first look at Main’s render function. It renders our Counter component with passed in values for props. Specifically, it passes in its store state for prop value.
On increment, it passes in an anonymous function that calls store.dispatch, which executes an action object with action type of INCREMENT or DECREMENT.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function render() { ReactDOM.render( <Counter value={store.getState()} onIncrement={ () => { return storeDispatch('INCREMENT') } } onDecrement={ () => { return storeDispatch('DECREMENT') } } />, document.getElementById('root') ) } |
So our action types match up with what we have in reducer.
Adding in Redux Saga
Redux saga acts as a middleware that gives developers the scope to neatly separate any business logic, Ajax requests, data manipulation or any other operation which may not seem appropriate in reducers directly.
Original Working without redux saga:-
Action(s) → Reducer(s)
With Redux saga as middleware:-
Action(s) → Redux Saga →Reducer(s)
Benefits of using Saga:
- Great support for Async Operations
- It takes care of basic needs like cancelling previous saga operation on start of new eg: cancel previous xhr call on typeahead search query.
- Can resolve a race between different effects (https://redux-saga.js.org/docs/advanced/RacingEffects/)
- Running tasks in parallel like Promise.all (https://redux-saga.js.org/docs/advanced/RunningTasksInParallel/)
- …and many other features
Let’s plug Saga into our simple application.
First, let’s create a Saga file.
Sagas are implemented as Generator functions that yield objects to the redux-saga middleware.
Saga
We implement a Promise that takes ms seconds to resolve. Once the Promise is resolved, the middleware will resume the Saga, executing code until the next yield.
1 2 3 4 |
import { put, takeEvery, all } from 'redux-saga/effects' // use a Promise that resolves after ms seconds to block the generator for ms seconds. const delay = (ms) => new Promise(res => setTimeout(res, ms)) |
We implement simple generator function that logs
1 2 3 |
export function* helloSaga() { console.log('Hello Sagas! 11:20') } |
We then create rootSaga function and yield all. This will execute all our saga functions when saga is run in main.js
In our example, we want to run helloSaga function that simply logs, and also execute a listener. This listener listens for action type INCREMENT_ASYNC.
1 2 3 4 5 6 |
export default function* rootSaga() { yield all([ helloSaga(), // display log watchIncrementAsync() // starts listening ]) } |
Let’s implement this watchIncrementAsync
We use takeEvery, a helper function provided by redux-saga, to listen for store.dispatch({INCREMENT_ASYNC}) and run incrementAsync each time.
1 2 3 4 5 6 |
export function* watchIncrementAsync() { console.log('sagas.js - watchIncrementAsync is now listening for INCREMENT_ASYNC') console.log('when it detects INCREMENT_ASYNC, incrementAsync is called') yield takeEvery('INCREMENT_ASYNC', incrementAsync) } |
As you can see, it’s a listener that listens for INCREMENT_ASYNC. Once INCREMENT_ASYNC is dispatched by store.dispatch, then we execute incrementAsync.
Let’s implement incrementAsync
As mentioned previously, middleware will suspend the Saga until the Promise completes.
put is one example of what we call an Effect.
Effects are plain JavaScript objects which contain instructions to be fulfilled by the middleware.
put instructs the middleware to dispatch an action to the Store.
When a middleware retrieves an Effect yielded by a Saga, the Saga is paused until the Effect is fulfilled.
1 2 3 4 5 6 |
export function* incrementAsync() { yield delay(1000) // We suspend Saga for 1 second // put dispatches action INCREMENT // put == return store.dispatch('INCREMENT') yield put({ type: 'INCREMENT' }) } |
The action INCREMENT will reach reducer, where reducer will return state + 1.
Since the store is subscribed to the render, once the state changes, it will redraw and we’ll get an updated value.