Home Development of Websites Figuring out redux-saga: From action generators to sagas

Figuring out redux-saga: From action generators to sagas

by admin

Figuring out redux-saga: From action generators to sagas
Any redux developer will tell you that one of the hardest parts of application development is asynchronous calls – how will you handle reqs, timeouts and other callbacks without complicating redux actions and reducers.
In this article I will describe several different approaches to managing asynchrony in your application, ranging from simple approaches like redux-thunkto more advanced libraries like redux-saga.
We’re going to use React and Redux, so we’ll assume you have at least some idea of how they work.

Action creators

API interaction is a common requirement in applications. Imagine we need to show a random picture of a dog when we click a button.
Figuring out redux-saga: From action generators to sagas
we can use Dog CEO API and something pretty simple like a fetch call inside the action creator.

const {Provider, connect} = ReactRedux;const createStore = Redux.createStore// Reducerconst initialState = {url: '', loading: false, error: false, };const reducer = (state = initialState, action) => {switch (action.type) {case 'REQUESTED_DOG':return {url: '', loading: true, error: false, };case 'REQUESTED_DOG_SUCCEEDED':return {url: action.url, loading: false, error: false, };case 'REQUESTED_DOG_FAILED':return {url: '', loading: false, error: true, };default:return state;}};// Action Creatorsconst requestDog = () => {return { type: 'REQUESTED_DOG' }};const requestDogSuccess = (data) => {return { type: 'REQUESTED_DOG_SUCCEEDED', url: data.message }};const requestDogaError = () => {return { type: 'REQUESTED_DOG_FAILED' }};const fetchDog = (dispatch) => {dispatch(requestDog());return fetch('https://dog.ceo/api/breeds/image/random').then(res => res.json()).then(data => dispatch(requestDogSuccess(data)), err => dispatch(requestDogError()));};// Componentclass App extends React.Component {render () {return (<div><button onClick={() => fetchDog(this.props.dispatch)}> Show Dog</button>{this.props.loading? <p> Loading...</p>: this.props.error? <p> Error, try again</p>: <p> <img src={this.props.url}/> </p> }</div>)}}// Storeconst store = createStore(reducer);const ConnectedApp = connect((state) => {console.log(state);return state;})(App);// Container componentReactDOM.render(<Provider store={store}><ConnectedApp /></Provider> , document.getElementById('root'));

jsfiddle.net/eh3rrera/utwt4dr8
There is nothing wrong with this approach. All other things being equal, it is always better to use a simpler approach.
However, using only Redux does not give us enough flexibility. The Redux kernel is a state container, which only supports synchronous data streams.
For each action, an object describing what happened is sent to the store, then the reducer is called and the state is updated immediately.
But in case of an asynchronous call, you have to wait for the response first and then, if there are no errors, update the state. And what if your application has some complicated logic/workflow?
To do this, Redux uses middlewares. A middlewares is a piece of code that is executed after the action is sent, but before the reduser is called.
Intermediate layers can connect into a chain of calls for various actions, but the output must always be a simple object (action)
For asynchronous operations, Redux suggests using redux-thunk intermediate layer.

Redux-thunk

Redux-thunk is the standard way to perform asynchronous operations in Redux.
For our purpose, redux-thunk introduces the notion of a converter(thunk), which is a function that provides delayed execution as needed.
Let’s take an example from redux-thunk documentation

let x = 1 + 2;

The value 3 is immediately assigned to the variable x.
However, if we have an expression like

let foo = () => 1 + 2;

Then the summation is not performed immediately, but only when foo() is called. This makes the foo function a transducer(thunk).
Redux-thunk allows the action generator (action creator) to send a function in addition to the object, thus converting the action generator to a transducer.
Below, we rewrite the previous example using redux-thunk

const {Provider, connect} = ReactRedux;const {createStore, applyMiddleware} = Redux;const thunk = ReduxThunk.default;// Reducerconst initialState = {url: '', loading: false, error: false, };const reducer = (state = initialState, action) => {switch (action.type) {case 'REQUESTED_DOG':return {url: '', loading: true, error: false, };case 'REQUESTED_DOG_SUCCEEDED':return {url: action.url, loading: false, error: false, };case 'REQUESTED_DOG_FAILED':return {url: '', loading: false, error: true, };default:return state;}};// Action Creatorsconst requestDog = () => {return { type: 'REQUESTED_DOG' }};const requestDogSuccess = (data) => {return { type: 'REQUESTED_DOG_SUCCEEDED', url: data.message }};const requestDogError = () => {return { type: 'REQUESTED_DOG_FAILED' }};const fetchDog = () => {return (dispatch) => {dispatch(requestDog());fetch('https://dog.ceo/api/breeds/image/random').then(res => res.json()).then(data => dispatch(requestDogSuccess(data)), err => dispatch(requestDogError()));}};// Componentclass App extends React.Component {render () {return (<div><button onClick={() => this.props.dispatch(fetchDog())}> Show Dog</button>{this.props.loading? <p> Loading...</p>: this.props.error? <p> Error, try again</p>: <p> <img src={this.props.url}/> </p> }</div>)}}// Storeconst store = createStore(reducer, applyMiddleware(thunk));const ConnectedApp = connect((state) => {console.log(state);return state;})(App);// Container componentReactDOM.render(<Provider store={store}><ConnectedApp /></Provider> , document.getElementById('root'));

jsfiddle.net/eh3rrera/0s7b54n4
At first glance it does not look much different from the previous version.
Without redux-thunk
Figuring out redux-saga: From action generators to sagas
With redux-thunk
Figuring out redux-saga: From action generators to sagas
The advantage of using redux-thunk is that the component does not know that an asynchronous action is being performed.
Since the intermediate layer automatically passes the dispatch function to the function that the action generator returns, there is no difference in calling synchronous and asynchronous actions outside for the component (and components no longer need to worry about this)
So with the mechanism of intermediate layers, we added an implicit layer (a layer of indirection) which gave us more flexibility.
Because redux-thunk passes the dispatch and getState methods from the store as parameters to the return functions, you can send other actions and use state to implement additional logic and workflow.
But what if we have something more complex to be expressed with a transducer (thunk), without changing the react component. In that case we can try to use another middleware library and get more control.
Let’s see how to replace redux-thunk with a library that can give us more control – redux-saga.

Redux-saga

Redux-saga is a library aimed at making side effects easier and better by working with sagas.
Sagi is a design pattern that comes from the world of distributed transactions, where sagi manages the processes that need to be executed in a transactional way, preserving execution state and compensating for failed processes.
To learn more about the sagas, you can start by watching Applications of the Saga pattern from Caitie McCaffrey , well, if you’re ambitious, here Article , which is the first to describe sagas with respect to distributed systems.
In the Redux context, saga is implemented as an intermediate layer (we can’t use redusers because they have to be pure functions) that coordinates and induces asynchronous actions (side-effects).
Redux-saga does this with ES6 generators
Figuring out redux-saga: From action generators to sagas
Generators are functions that can be stopped and continued, instead of executing all expressions in one go.
When you call a generator function, it returns an iterator object. And with each call to the iterator next() method, the body of the generator function will execute until the next yield expression and then stop.
Figuring out redux-saga: From action generators to sagas
This makes asynchronous code easier to write and understand.
For an example, instead of the following expression :
Figuring out redux-saga: From action generators to sagas
With generators we would write like this :
Figuring out redux-saga: From action generators to sagas
Going back to redux-saga, generally speaking, we have a saga whose job is to keep track of dispatched actions.
Figuring out redux-saga: From action generators to sagas
To coordinate the logic we want to implement inside the saga, we can use the auxiliary function takeEvery to create a new saga to perform the operation.
Figuring out redux-saga: From action generators to sagas
If there are multiple requests, takeEvery starts multiple instances of worker saga. In other words, it implements concurrency for you.
Note that watcher saga is another implicit layer (layer of indirection) which gives more flexibility to implement complex logic (but this may be unnecessary for simple applications).
Now we can implement the fetchDogAsync() function (we assume that we have access to the dispatch method)
Figuring out redux-saga: From action generators to sagas
But redux-saga allows us to get an object that declares our intention to perform an operation, instead of the result of the operation itself. In other words, the example above is implemented in redux-saga as follows :
Figuring out redux-saga: From action generators to sagas
(Translator’s note : the author forgot to replace the very first dispatch call)
Instead of calling the asynchronous requeue directly, the callmethod will only return an object describing this operation and redux-saga can take care of calling and returning the results to the generating function.
The same is true for the putmethod.Instead of a dispatch action inside the generator function, put returns an object with instructions for the middleware to send an action.
These returned objects are called Effects. Below is an example of an effect returned by the call method:
Figuring out redux-saga: From action generators to sagas
Working with Effects, redux-saga makes sagas more likely Declarative than Imperative
Declarative programming is a style of programming that attempts to minimize or eliminate side-effects by describing that program should do, instead of describing as it should do.
The advantage this has, and what most people talk about, is that a function that returns a simple object is much easier to test than a function that makes an asynchronous call. To test, you don’t have to use a real API, make fakes, or take a piss.
To test, you simply iterate the generator function doing assert and compare the values obtained.
Figuring out redux-saga: From action generators to sagas
Another added benefit is the ability to easily combine different effects into a complex workflow.
In addition to takeEvery , call , put , redux-saga offers many effects creator methods (Effects creators) for delays , getting the current state , running concurrent tasks , and task cancellations Just to note a few possibilities.
Returning to our simple example, below is the full implementation in redux-saga:

const {Provider, connect} = ReactRedux;const {createStore, applyMiddleware} = Redux;const createSagaMiddleware = ReduxSaga.default;const {takeEvery} = ReduxSaga;const {put, call} = ReduxSaga.effects;// Reducerconst initialState = {url: '', loading: false, error: false, };const reducer = (state = initialState, action) => {switch (action.type) {case 'REQUESTED_DOG':return {url: '', loading: true, error: false, };case 'REQUESTED_DOG_SUCCEEDED':return {url: action.url, loading: false, error: false, };case 'REQUESTED_DOG_FAILED':return {url: '', loading: false, error: true, };default:return state;}};// Action Creatorsconst requestDog = () => {return { type: 'REQUESTED_DOG' }};const requestDogSuccess = (data) => {return { type: 'REQUESTED_DOG_SUCCEEDED', url: data.message }};const requestDogError = () => {return { type: 'REQUESTED_DOG_FAILED' }};const fetchDog = () => {return { type: 'FETCHED_DOG' }};// Sagasfunction* watchFetchDog() {yield takeEvery('FETCHED_DOG', fetchDogAsync);}function* fetchDogAsync() {try {yield put(requestDog());const data = yield call(() => {return fetch('https://dog.ceo/api/breeds/image/random').then(res => res.json())});yield put(requestDogSuccess(data));} catch (error) {yield put(requestDogError());}}// Componentclass App extends React.Component {render () {return (<div><button onClick={() => this.props.dispatch(fetchDog())}> Show Dog</button>{this.props.loading? <p> Loading...</p>: this.props.error? <p> Error, try again</p>: <p> <img src={this.props.url}/> </p> }</div>)}}// Storeconst sagaMiddleware = createSagaMiddleware();const store = createStore(reducer, applyMiddleware(sagaMiddleware));sagaMiddleware.run(watchFetchDog);const ConnectedApp = connect((state) => {console.log(state);return state;})(App);// Container componentReactDOM.render(<Provider store={store}><ConnectedApp /></Provider> , document.getElementById('root'));

jsfiddle.net/eh3rrera/qu42h5ee
When you press the button, this is what happens :
Action FETCHED_DOG is sent
2. The watcher saga watchFetchDog receives this action and calls the worker saga fetchDogAsync.
3. the load indicator action is sent.
4. The API method is called.
5. A status update action is sent (success or failure)
If you think a few implicit layers and a little extra work are worth it, redux-saga can give you more control to handle side effects in a functional way.

Conclusion

This article showed how to implement asynchronous operations in Redux using action creators, thunks, and sagas, going from a simple approach to a more complex one.
Redux does not prescribe a solution for handling side effects. When you decide which approach to follow, you need to consider the complexity of your application. My recommendation is to start with a simple solution.
There are also alternatives to redux-saga that are worth trying. Two of the most popular are redux-observable (which is based on RxJS ) and redux-logic (also based on RxJS observers, but giving you the freedom to write your logic in other styles ).

You may also like