Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pros/cons of using redux-saga with ES6 generators vs redux-thunk with ES2017 async/await

There is a lot of talk about the latest kid in redux town right now, redux-saga/redux-saga. It uses generator functions for listening to/dispatching actions.

Before I wrap my head around it, I would like to know the pros/cons of using redux-saga instead of the approach below where I'm using redux-thunk with async/await.

A component might look like this, dispatch actions like usual.

import { login } from 'redux/auth';  class LoginForm extends Component {    onClick(e) {     e.preventDefault();     const { user, pass } = this.refs;     this.props.dispatch(login(user.value, pass.value));   }    render() {     return (<div>         <input type="text" ref="user" />         <input type="password" ref="pass" />         <button onClick={::this.onClick}>Sign In</button>     </div>);   }  }  export default connect((state) => ({}))(LoginForm); 

Then my actions look something like this:

// auth.js  import request from 'axios'; import { loadUserData } from './user';  // define constants // define initial state // export default reducer  export const login = (user, pass) => async (dispatch) => {     try {         dispatch({ type: LOGIN_REQUEST });         let { data } = await request.post('/login', { user, pass });         await dispatch(loadUserData(data.uid));         dispatch({ type: LOGIN_SUCCESS, data });     } catch(error) {         dispatch({ type: LOGIN_ERROR, error });     } }  // more actions... 

// user.js  import request from 'axios';  // define constants // define initial state // export default reducer  export const loadUserData = (uid) => async (dispatch) => {     try {         dispatch({ type: USERDATA_REQUEST });         let { data } = await request.get(`/users/${uid}`);         dispatch({ type: USERDATA_SUCCESS, data });     } catch(error) {         dispatch({ type: USERDATA_ERROR, error });     } }  // more actions... 
like image 769
hampusohlsson Avatar asked Jan 21 '16 17:01

hampusohlsson


People also ask

Is redux saga better than Thunk?

The benefit of Redux-Saga in comparison to Redux-Thunk is that you can more easily test your asynchronous data flow. Redux-Thunk, however, is great for small projects and for developers who just entered into the React ecosystem. The thunks' logic is all contained inside of the function.

When should I use redux saga?

Redux Saga is a middleware library used to allow a Redux store to interact with resources outside of itself asynchronously. This includes making HTTP requests to external services, accessing browser storage, and executing I/O operations. These operations are also known as side effects.

Can we use async await in redux saga?

You can transpile async/await into generators, but you can't do the reverse. As a userland library, redux-saga can handle asynchronous behavior in ways that async/await doesn't.

What is the difference between Redux-Thunk and redux saga which one should I use How do you decide between thunks sagas is it fine to use both?

Saga works like a separate thread or a background process that is solely responsible for making your side effects or API calls unlike redux-thunk, which uses callbacks which may lead to situations like 'callback hell' in some cases. However, with the async/await system, this problem can be minimized in redux-thunk.


2 Answers

In redux-saga, the equivalent of the above example would be

export function* loginSaga() {   while(true) {     const { user, pass } = yield take(LOGIN_REQUEST)     try {       let { data } = yield call(request.post, '/login', { user, pass });       yield fork(loadUserData, data.uid);       yield put({ type: LOGIN_SUCCESS, data });     } catch(error) {       yield put({ type: LOGIN_ERROR, error });     }     } }  export function* loadUserData(uid) {   try {     yield put({ type: USERDATA_REQUEST });     let { data } = yield call(request.get, `/users/${uid}`);     yield put({ type: USERDATA_SUCCESS, data });   } catch(error) {     yield put({ type: USERDATA_ERROR, error });   } } 

The first thing to notice is that we're calling the api functions using the form yield call(func, ...args). call doesn't execute the effect, it just creates a plain object like {type: 'CALL', func, args}. The execution is delegated to the redux-saga middleware which takes care of executing the function and resuming the generator with its result.

The main advantage is that you can test the generator outside of Redux using simple equality checks

const iterator = loginSaga()  assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))  // resume the generator with some dummy action const mockAction = {user: '...', pass: '...'} assert.deepEqual(   iterator.next(mockAction).value,    call(request.post, '/login', mockAction) )  // simulate an error result const mockError = 'invalid user/password' assert.deepEqual(   iterator.throw(mockError).value,    put({ type: LOGIN_ERROR, error: mockError }) ) 

Note we're mocking the api call result by simply injecting the mocked data into the next method of the iterator. Mocking data is way simpler than mocking functions.

The second thing to notice is the call to yield take(ACTION). Thunks are called by the action creator on each new action (e.g. LOGIN_REQUEST). i.e. actions are continually pushed to thunks, and thunks have no control on when to stop handling those actions.

In redux-saga, generators pull the next action. i.e. they have control when to listen for some action, and when to not. In the above example the flow instructions are placed inside a while(true) loop, so it'll listen for each incoming action, which somewhat mimics the thunk pushing behavior.

The pull approach allows implementing complex control flows. Suppose for example we want to add the following requirements

  • Handle LOGOUT user action

  • upon the first successful login, the server returns a token which expires in some delay stored in a expires_in field. We'll have to refresh the authorization in the background on each expires_in milliseconds

  • Take into account that when waiting for the result of api calls (either initial login or refresh) the user may logout in-between.

How would you implement that with thunks; while also providing full test coverage for the entire flow? Here is how it may look with Sagas:

function* authorize(credentials) {   const token = yield call(api.authorize, credentials)   yield put( login.success(token) )   return token }  function* authAndRefreshTokenOnExpiry(name, password) {   let token = yield call(authorize, {name, password})   while(true) {     yield call(delay, token.expires_in)     token = yield call(authorize, {token})   } }  function* watchAuth() {   while(true) {     try {       const {name, password} = yield take(LOGIN_REQUEST)        yield race([         take(LOGOUT),         call(authAndRefreshTokenOnExpiry, name, password)       ])        // user logged out, next while iteration will wait for the       // next LOGIN_REQUEST action      } catch(error) {       yield put( login.error(error) )     }   } } 

In the above example, we're expressing our concurrency requirement using race. If take(LOGOUT) wins the race (i.e. user clicked on a Logout Button). The race will automatically cancel the authAndRefreshTokenOnExpiry background task. And if the authAndRefreshTokenOnExpiry was blocked in middle of a call(authorize, {token}) call it'll also be cancelled. Cancellation propagates downward automatically.

You can find a runnable demo of the above flow

like image 172
Yassine Elouafi Avatar answered Oct 29 '22 04:10

Yassine Elouafi


I will add my experience using saga in production system in addition to the library author's rather thorough answer.

Pro (using saga):

  • Testability. It's very easy to test sagas as call() returns a pure object. Testing thunks normally requires you to include a mockStore inside your test.

  • redux-saga comes with lots of useful helper functions about tasks. It seems to me that the concept of saga is to create some kind of background worker/thread for your app, which act as a missing piece in react redux architecture(actionCreators and reducers must be pure functions.) Which leads to next point.

  • Sagas offer independent place to handle all side effects. It is usually easier to modify and manage than thunk actions in my experience.

Con:

  • Generator syntax.

  • Lots of concepts to learn.

  • API stability. It seems redux-saga is still adding features (eg Channels?) and the community is not as big. There is a concern if the library makes a non backward compatible update some day.

like image 22
yjcxy12 Avatar answered Oct 29 '22 04:10

yjcxy12