I am a Redux newbie and just came across redux-thunk (and saga). Just wondering why should I use them and what the real benefits are!

RE:

Predictability and Side-effects

Redux advertises itself as a predictable state container.

This predictability comes at a cost: Your actions that are supposed to manipulate the state of your application are processed synchronously through pure functions called reducers. You should not have side-effects in your reducers.

However, in real world applications you will have to deal with side-effects somewhere - you will need to make network calls, take decisions based on results of API responses, retry in case of errors etc.

Let us try to envision the flow of operations around a typical user registration form. When user submits a registration form, you want to submit the form to the server, show a loader along with a message that the form is being submitted, and remove the loader and show the user's dashboard once the submission is successful. I will assume that React is also being used, but most of the concepts below will apply to other frontend libraries as well.

So when the user hits submit button, the form component dispatches an action containing the payload. The typical code for this would like the following (the code below is quite verbose, but you can utilize libraries to reduce the noise):

// actions.js

/**
 * Action to be dispatched to indication that form has been submitted by user
 */
export const registrationFormSubmissionInitiated = (payload) => ({
    type: 'REGISTRATION:SUBMIT:INIT',
    payload
});

/**
 * Action to be dispatched to indication that form was successfully submitted
 */
export const registrationFormSubmissionSuccessful = (payload) => ({
    type: 'REGISTRATION:SUBMIT:SUCCESS',
    payload
});

/**
 * Action to be dispatched to indication that form submission failed.
 */
export const registrationFormSubmissionFailed = (payload) => ({
    type: 'REGISTRATION:SUBMIT:FAIL',
    payload
});
// RegistrationForm.js
import React, {Component, PropTypes} from 'react';

export default class RegistrationForm extends Component {
    static propTypes = {
        onSubmit: PropTypes.func.isRequired
    }
    render() {
        // The onSubmit prop will be provided by our redux connected component
         // which will have access to dispatch
        return (
            <form onSubmit={this.props.onSubmit}>
                {/* form fields go here */}
            </form>
        );
    }
}
// RegistrationFormContainer.js
import {connect} from 'react-redux';
import RegistrationForm from './RegistrationForm';
import {registrationFormSubmissionInitiated} from './actions';

const mapStateToProps = (state) => {...}

/**
 * Make action dispatcher available to React component through a function prop.
 */
const mapDispatchToProps = (dispatch) => ({
    onSubmit: (data) => dispatch(registrationFormSubmissionInitiated(data))
});

const RegistrationFormContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(RegistrationForm);

export default RegistrationFormContainer;

So now what happens in the reducer?

import {Map} from 'immutable';

const INITIAL_STATE = Map.fromJS({
    notifications: []
});

const reducer = (state = INITIAL_STATE, action) => {
    switch (action.type) {
        case 'REGISTRATION:SUBMIT:INIT':
             // Update notification list
             state = state.set('notifications', state.get('notifications').push({
                 message: "Form is being submitted",
                 isModal: true
             }))
    }
    return state;
}

So far so good. If action is dispatched we have updated a notification in the state, which some UI component (not described) can use to show a modal notification to user. However we are still missing the crucial piece: where do we communicate with the server ?

Side effects do not belong in reducers

Your first instinct might be to do it in a reducer. This is strictly not recommended.

If you perform an ajax request in a reducer, thereby introducing a side-effect -- you break predictability of redux.

How so ? If you are using time travelling debugger then if you travel backward and forward in time - the same action would be handled by the same reducer twice, and you would end up making the request twice and the latter would break because the user already exists. So you do not end up with the same state and hence your time travelling debugger is now pretty much useless.

Much of redux ecosystem leverages this kind of predictability in some way or other.

Alternative state containers

If you don't need these features - consider not using redux and using other kinds of state containers. What other kinds ? You have plain javascript data structures (if you don't need reactivity either) or observables (if you need reactivity but not predictability). MobX is good solution in the latter category that has recently gained much adoption in the community.

Side-effects in Redux applications

If you are still reading this, I will assume that you want predictability and are going with Redux. So now that we have dismissed side effects in reducers, where do we put them ? The next best thing you can come up with is putting them in a higher order component.

Managing side effects through Higher order components

Higher-Order Components (HOCs) are JavaScript functions which add functionality to existing component classes. Just as React components let you add functionality to an application, Higher-Order Components let you add functionality to components. You could say they’re components for components.

// RegistrationFormManager.js
import React, {Component} from 'react';
import {bindAll} from 'lodash';
import RegistrationForm from './RegistrationForm';
import request from 'axios';

class FormManager extends Component {

    static propTypes = {
        submissionInitiated: PropTypes.func.isRequired,
        submissionSuccessful: PropTypes.func.isRequired,
        submissionFailed: PropTypes.func.isRequired
    }

    constructor(...args) {
        super(...args);
        bindAll(this, ['handleSubmit']);
    }
    render() {
        return <RegistrationForm onSubmit={this.handleSubmit}/>;
    }
    handleSubmit(data) {
        this.props.submissionInitiated(data);
        request.post('/api/users', data)
             .then(({body}) => this.submissionSuccessful(body))
             .catch((error) => this.submissionFailed(error));
    }
}

And we can move our RegistrationFormContainer to RegistrationFormManagerContainer:

// RegistrationFormManagerContainer.js
import {connect} from 'react-redux';
import RegistrationFormManager from './RegistrationFormManager';
import {registrationFormSubmissionInitiated,registrationFormSubmissionSuccessful, registrationFormSubmissionFailed} from './actions';

const mapStateToProps = (state) => {...}
const mapDispatchToProps = (dispatch) => ({
    submissionInitiated: (data) => dispatch(registrationFormSubmissionInitiated(data)),
    submissionSuccessful: (data) => dispatch(registrationFormSubmissionSuccessful(data)),
    submissionFailed: (data) => dispatch(registrationFormSubmissionFailed(data))
});

const RegistrationFormContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(RegistrationFormManager);

export default RegistrationFormContainer;

This is pretty good. And if this fulfils your use case you may very well stop here. However this is not ideal for all cases.

Caveats with using HOCs for side-effects

Let us try to imagine another situation where we need to load notifications for comments from users. So when we load new notifications we need to update a counter on the top navbar as well as show new comments in the comment list. So now where do we put the logic for fetching notifications ? In a higher order component ? What does the component wrap ? The notification counter component or the component list that shows comments ?

If you put some thought in this you would realise that you should have never had to make this decision in the first place ? Why ? Because the responsibility of managing communication with server has nothing to do with React, has nothing to with our component hierarchy and should not be coupled with React component lifecycles at all. Modelling things that have nothing to do with presentation as components, was a mistake in the first place.

This is where solutions like redux-thunk and redux-saga come into picture.

Dedicated solutions for side-effect management

What solutions like redux-thunk and redux-saga bring to table (albeit in different ways) are facilities to manage and orchestrate your side effects, integrated with redux but outside and decoupled from your presentation layer.

Using thunks for side effects

Redux thunk puts the intelligence that drives asynchronous operations in your application in action creators. So far our action creators (registrationFormSubmissionInitiated,registrationFormSubmissionSuccessful, registrationFormSubmissionFailed) were unapologetically dumb. Get a payload, dispatch an action then and there. done.

Now they get to decide what to dispatch, when to dispatch. After you have installed redux-thunk you can write something like this:

// actions.js

export const registrationFormSubmitted = (formData) => 
    (dispatch) => {
        dispatch(registrationFormSubmissionInitiated(formData))
        request.post('/api/users', data)
             .then(({body}) => dispatch(registrationFormSubmissionSuccessful(body)))
             .catch((error) => dispatch(registrationFormSubmissionFailed(error)))
    }

So now we can throw away our Higher order component for managing communication, and allow our action creator to conduct our async operation and dispatch actions accordingly.

Above actions also illustrate how a smart action creator can delegate to other dumb action creators and they can co-exist harmoniously.

Our registration form can consume this much like it did before:

// RegistrationForm.js --- Zero change here
import React, {Component, PropTypes} from 'react';

export default class RegistrationForm extends Component {
    static propTypes = {
        onSubmit: PropTypes.func.isRequired
    }
    render() {
        return (
            <form onSubmit={this.props.onSubmit}>
                {/* form fields go here */}
            </form>
        );
    }
}
// RegistrationFormContainer.js
import {connect} from 'react-redux';
import RegistrationForm from './RegistrationForm';
import {registrationFormSubmitted} from './actions';

const mapStateToProps = (state) => {...}
const mapDispatchToProps = (dispatch) => ({
    onSubmit: (data) => dispatch(registrationFormSubmitted(data))
});

const RegistrationFormContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(RegistrationForm);

export default RegistrationFormContainer;

So far so good. We removed the coupling, separated our the async logic somewhere else and have a clean architecture. Are we done ? Can we call it a day ?

Well, there is a reason redux-saga exists.

A detour into javascript generators

Now would be a good time to familiarize ourselves with javascript generators:

From MDN :

A generator is a special type of function that works as a factory for iterators. A function becomes a generator if it contains one or more yield expressions and if it uses the function* syntax.

function* idMaker(){
  var index = 0;
  while(true)
    yield index++;
}

var gen = idMaker();

console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
// ...

Generators really have nothing to do with redux, react etc. It is a new feature in javascript (that can be polyfilled efficiently) that helps you with async control flow. While that may not be obvious from the example above, but one thing is clearly visible:

The callee can communicate back to caller at any arbitrary point of execution and caller can choose to resume the execution from that point at any point in future. This effectively allows you to voluntarily suspend execution mid way and hand over control to someone else who can resume the control at will. This is huge. And libraries like co have utilized this potential to simplify asynchronous logic in the backend.

Generators, Sagas and complex chains of asynchronous operations

Redux saga does something similar for your frontend code:

The mental model is that a saga is like a separate thread in your application that's solely responsible for side effects. redux-saga is a redux middleware, which means this thread can be started, paused and cancelled from the main application with normal redux actions, it has access to the full redux application state and it can dispatch redux actions as well.

So while for a huge majority of cases, redux-thunks are all you need, and are what I would recommend you to get started with. But if you find yourself building complex applications where you need to orchestrate a multitude of asynchronous operations and need a separate layer to dedicatedly manage lifecycle of these asynchronous operations then redux-saga is what you need.

On a very high level, this kind of solution is particularly helpful if you want an action happening in present, to impact the logic for handling other actions in future. Or have separate multi-step asynchronous operations that need to communicate among themselves.

To wrap up, advanced features of Redux-saga simplify implementation of use cases like:

  • In a large multi-user, multi-department blog, if a user had ever clicked on a "subscribe to RSS Feed" button, then the next time the user visits a section that has a dedicated RSS feed, show him/her a suggestion to subscribe to this section's feed.

  • In an online IDE, if a user has never used a particular feature of an application, but has arrived at a state that the aforementioned feature might potentially be useful, show a help dialog introducing this feature.

  • In stackoverflow, while the user was responding to a question, the question has been changed by OP so you inform the user that the question has been changed and the answer is potentially no longer valid.

etc.

Generators and promises, sans Sagas

Last but not the least, you can still use generators to manage your async control flow, in frontend, without adding redux-thunk dependency, if you do not need some of its advanced features.

export const registrationFormSubmitted = formData => 
  dispatch =>
      co(function* () {
        dispatch(registrationFormSubmissionInitiated(formData));
        try {
          const {body} = yield request.post('/api/users', formData);
          dispatch(registrationFormSubmissionSuccessful(body));
        }
        catch (error) {
          dispatch(registrationFormSubmissionFailed(error));
        }
    });

As you can see that now you get to use javascript exception handling constructs and get rid of another level of indentation.

As co returns promises, you can further reduce some verbosity by throwing in redux-promise which will automatically detect whether a dispatched promise has been fulfilled or not, and accordingly dispatch completion failure or completion success actions according to a pre-specified convention.

I guess that gives you a pretty good idea about handling side-effects in Redux ecosystem. Thanks for reading this (unexpectedly long) post. If you have any questions, please let me know.

Show all replies

If your application/workflow really benefits from hot-reloading & time-travel go with redux. If not, strongly consider MobX.

I have been using MobX over last two months and have found myself much more productive with Typescript+React+Inversify+MobX combo (TRIM stack, anyone ?).

I do not foresee myself picking redux over MobX for new projects in near future.

Reply to this…

(2 answers) Take me to the question

The Author Card

John Martin's photo

John Martin

Programmer and tinkerer.

Appreciations

175

Joined

Mar 28, 2016