📜 ⬆️ ⬇️

Understanding redux-saga: From action generators to sagas



Any redux developer will tell you that one of the most difficult parts of application development is asynchronous calls - how will you handle requests, 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-thunk, to more advanced libraries like redux-saga.

We are going to use React and Redux, so we will assume that you have at least some idea of ​​how they work.
')

Action creators


Interaction with the API is quite a frequent requirement in applications. Imagine that we need to show a random picture of a dog when we press a button.



we can use the Dog CEO API and something rather simple, like calling the fetch inside the action creator.

const {Provider, connect} = ReactRedux; const createStore = Redux.createStore // Reducer const 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 Creators const 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()) ); }; // Component class 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> ) } } // Store const store = createStore(reducer); const ConnectedApp = connect((state) => { console.log(state); return state; })(App); // Container component ReactDOM.render( <Provider store={store}> <ConnectedApp /> </Provider>, document.getElementById('root') ); 

jsfiddle.net/eh3rrera/utwt4dr8

There is nothing wrong with this approach. 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 core is a state container that only supports synchronous data streams.

For each action, an object describing what happened is sent to the store (store), then the reducer is called and the state is immediately updated.

But in the case of an asynchronous call, you must first wait for a response and then, if there were no errors, update the state. And what if your application has some complex logic / workflow?

Redux uses middlewares for this. The intermediate layer is a piece of code that is executed after the action is sent, but before the call to the reducer.
Intermediate layers can be connected in a chain of calls for different processing of the action (action), but the output must necessarily be a simple object (action)

For asynchronous operations, Redux suggests using a redux-thunk intermediate layer.

Redux-thunk


Redux-thunk is the standard way to perform asynchronous operations on Redux.
For our purpose, redux-thunk introduces the notion of thunk, which is a function that provides deferred execution, as needed.

Take an example from the 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; 

That summation is not performed immediately, but only when the foo () function is called. This makes the foo function a thunk.

Redux-thunk allows the action creator to send a function in addition to the object, thus converting the action generator to the converter.

Below, we rewrite the previous example using redux-thunk

 const {Provider, connect} = ReactRedux; const {createStore, applyMiddleware} = Redux; const thunk = ReduxThunk.default; // Reducer const 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 Creators const 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()) ); } }; // Component class 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> ) } } // Store const store = createStore( reducer, applyMiddleware(thunk) ); const ConnectedApp = connect((state) => { console.log(state); return state; })(App); // Container component ReactDOM.render( <Provider store={store}> <ConnectedApp /> </Provider>, document.getElementById('root') ); 

jsfiddle.net/eh3rrera/0s7b54n4

At first glance, it is not much different from the previous version.

Without redux-thunk



With redux-thunk



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, then outside, for the component, there is no difference in calling synchronous and asynchronous actions (and components no longer need to worry about it)

Thus, using the intermediate layers mechanism, we added an implicit layer (a layer of indirection), which gave us more flexibility.

Since redux-thunk passes dispatch and getState from the store as parameters to return functions, you can send other actions and use the state to implement additional logic and workflow.

But what if we have something more complicated to be expressed using a thunk, without changing the react component. In this case, we can try to use another middleware library and get more control.

Let's see how to replace redux-thunk with a library, which 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.

Saga is a design pattern that comes from the world of distributed transactions, where the saga manages the processes that need to be executed in a transactional way, maintaining the execution state and compensating for failed processes.

To learn more about sagas, you can start by looking at the Application of the Saga Pattern by Caitie McCaffrey , but if you are ambitious, here is the article that first describes the sagas for distributed systems.

In the context of Redux, the saga is implemented as an intermediate layer (we can’t use reducer because they have to be pure functions), which coordinates and induces asynchronous actions (side effects).

Redux-saga does it with ES6 generators



Generators (Generators) are functions that can be stopped and continued, instead of executing all expressions in a single pass.

When you call a generator function, it returns an iterator object. And with each call to the iterator method next (), the body of the generator function will execute until the next yield expression and then stop.



This makes asynchronous code easier to write and understand.
For example, instead of the following expression:



With generators, we would write this:



Returning to redux-saga, generally speaking, we have a saga whose job is to follow the dispatched actions.



To coordinate the logic that we want to implement inside the saga, we can use the auxiliary function takeEvery to create a new saga to perform the operation.



If there are multiple requests, takeEvery starts several instances of the worker saga (worker saga). In other words, realizes the competitiveness (concurrency) for you.

It should be noted that the watcher saga is another implicit layer (layer of indirection) that gives more flexibility to implement complex logic (but this may be superfluous for simple applications).

Now we can implement the fetchDogAsync () function (we assume that we have access to the dispatch method)



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:



(Approx. Translator: the author forgot to replace the very first call to dispatch)
Instead of calling an asynchronous request directly, the call method will return only the object describing this operation and redux-saga will be able to take care of the call and return the results to the generator function.

The same goes for the put method. Instead of sending a dispatch action inside a generator function, put returns an object with instructions for an intermediate layer (middleware) - send an action.

These returned objects are called Effects. Below is an example of the effect returned by the call method:



Working with Effects, redux-saga makes the sagas Declarative rather than Imperative .

Declarative programming is a programming style that tries to minimize or eliminate side effects, describing what a program should do, instead of describing how it should do it.

The advantage that this gives, and what most people are talking about, is that it is much easier to test a function that returns a simple object than a function that makes an asynchronous call. For testing, you do not need to use a real API, make fakes or get wet.

For testing, you simply iterate the function generator doing assert and compare the values ​​obtained.


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 methods that create effects (Effects creators) for delaying , getting the current state , starting parallel tasks , and canceling tasks . Just point out 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; // Reducer const 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 Creators const 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' } }; // Sagas function* 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()); } } // Component class 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> ) } } // Store const sagaMiddleware = createSagaMiddleware(); const store = createStore( reducer, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(watchFetchDog); const ConnectedApp = connect((state) => { console.log(state); return state; })(App); // Container component ReactDOM.render( <Provider store={store}> <ConnectedApp /> </Provider>, document.getElementById('root') ); 

jsfiddle.net/eh3rrera/qu42h5ee

When you press a button, this is what happens:

1. The action is sent FETCHED_DOG
2. The watcher saga watchFetchDog gets this action and calls the worker saga fetchDogAsync.
3. An action is sent to display the load indicator.
4. The method API call is made.
5. A status update action (success or failure) is sent.

If you think that a few implicit layers and a little bit of extra work are worth it, then redux-saga can give you more control for handling side effects in a functional way.

Conclusion


This article showed how to implement asynchronous operations in Redux using action generators (action creators), converters (thunks), and sagas (sagas), going from a simple approach to a more complex one.

Redux does not prescribe a solution for processing 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 redux-saga alternatives that are worth trying. The two most popular are redux-observable (which is based on RxJS ) and redux-logic (also based on RxJS observers, but giving the freedom to write your logic in other styles ).

Source: https://habr.com/ru/post/351168/


All Articles