Exploring redux-sagas

In the first of a two-part series, Onfido front-end engineer Kai Moseley explains how redux-sagas are shaking up the scene, and why they might be worth a look.

Redux-sagas are the hot new (or, if you're reading this a few months from now, the old school) way of working in the redux world. They solve any ailment that you might have in your application - or at least problems involving non-trivial updates you want to perform, or side effect that you don't want to tie to a component. At Onfido we use React/Redux and also encounter that issue, so it made sense to explore sagas and see how well they solved it in practice.

Why is this even necessary?

One issue that a React/Redux developer will encounter sooner or later is performing a change when a component does not naturally have all of the information available. While it's possible to just pollute the component with the extra data, it would be cleaner if we could avoid that entirely, allowing the component to remain focused on displaying data.

A popular solution to this problem is the redux-thunk library. Redux-thunk is a middleware which allows you to dispatch a 'thunk'. The thunk is a function which returns another function where dispatch and getState are passed in as arguments.

const aThunk = () => (dispatch, getState) => {  
  const someData = selector(getState());
  dispatch({ 
    type: SOME_ACTION,
    payload: someData, 
  });    
}

Once you have access to these two functions, you are free to do all kinds of fun stuff like select data from the store, dispatch actions, and perform AJAX requests, all without the component having to be aware of the implementation. The component can therefore get back to being focused on its primary job of displaying information to the user, and responding to interactions.

Let's say we have a component, Robot whose lot in life is to pass butter. It does this via displaying whether or not the butter.hasMoved props is true, and also dispatching an action, MOVE_BUTTER, which will set the butter.hasMoved value to true on the store:

import { selectButter } from 'someSelectorFile';  
import {  
  MOVE_BUTTER,
  QUESTION_PURPOSE,
} from 'someConstantsFile';  

const Robot = props =>  
  <div>
      { props.purpose || 'I pass butter' }
    { props.butter.hasMoved && 'I moved the butter!' }
    { props.butter.hasMoved || <Button onClick={ props.moveButter } /> }
    <Button onClick={ props.questionPurpose } />
  </div>;

const mapStateToProps = state => ({  
  butter: selectButter(state),
  purpose: selectPurpose(state),
});  

const mapDispatchToProps = dispatch => ({  
  moveButter() {
    dispatch({ 
        type: UPDATE_BUTTER,
        payload: { hasMoved: true }
     });
  },
  questionPurpose() {
       dispatch({ type: QUESTION_PURPOSE });
  },
});

export RobotContainer = connect(  
    mapStateToProps,
    mapDispatchToProps
)(Robot);    

Butter reducer shape:

{
    hasMoved: boolean,
}

It makes sense that the robot is aware of the data contained in the butter reducer. It needs to know whether or not the butter has already been moved in order to determine whether or not to allow the butter to be moved. The Robot isn't aware of exactly where this information is stored, as this particular detail is handled by the selectors.

So, time to take it further! It's entirely possible that another part of the application, let's say a reducer called genius and a related component called Scientist, have an interest in when the butter is moved. This reducer maintains an object, of which there is a property canPraiseRobot.

{ 
    canPraiseRobot: boolean,
}

When the robot has moved the butter, we would like to update this object so that the canPraiseRobot prop is set to false. If the Robot component were to perform this update, it will then know more about its surrounding environment than it needs to. After all, its job is to pass butter, not to try and make friends. However, that other part of the store still needs to be updated somehow.

Without any form of middle layer between the React layers and the reducers that will eventually handle the action, we're pretty limited on how to handle this functionality. A common solution (that doesn't simply leak implementation details of what happens after moving butter all over the poor robot) is for a parent component to contain all of the dispatches and selectors for both Scientist and Robot and pass them to both components. The moveButter function (now defined in the parent) will then be able to perform all of the necessary updates, without the robot needing to know about the actions required by the genius reducer:

    moveButter() {
        dispatch({ type: UPDATE_BUTTER, payload: { hasMoved: true } });
        dispatch({ type: UPDATE_GENIUS, payload: { canPraiseRobot: true } });
    }

While this can work in some situations, it isn't always ideal. The stateless components are still composable and reusable, but they are now linked by a parent providing the functionality we need. The implementation of functionality is dictating the position of components in the UI.

Depending on the situation (are the components naturally close together in the UI structure? are they several layers apart?), this may not even be feasible. Real applications can often reach this boundary, where either compromises are made (sometimes unconsciously), or different approaches are required.

What about context?

One other method of sharing data around in React is, of course, context (A popular example of context is react-redux's Provider). It's potentially very useful, but it's recommended you avoid using it unless you have specific reasons to do so (which is usually only going to be when creating a library, rather than 'application code'). Also, whilst critical to the functionality of pretty much any react/redux application, it's an unsupported feature which could, in theory, disappear one day.

A nicer solution is to take the burden of stitching together this kind of functionality from React. React already has to govern the UI structure, presenting data in a sane way. We need some form of glue code to link the React and Redux layers in a way that gives us freedom to perform the updates we need to build our fancy modern web application.

This glue can take many forms (RxJS observables, for example), and this is the layer where thunk/sagas reside. Both of these provide middlewares which hook into the dispatch functionality of redux, but go about handling dispatching and selecting of data in a different way. Both of these have access to getState and dispatch, with sagas opting to hide them behind 'effects', rather than directly exposing them. Both are able to take an action and perform any additional side effects needed in an application, including dispatching further actions. Instead of relying on passing unnecessary props around, or creating parent components which don't give you the freedom you need, you can instead create one function/generator which describes the functionality in it's entirety.

Below is an example of a saga doing just that:

import { put } from 'redux-saga/effects';  
import { takeEvery } from 'redux-saga';  
import {  
  MOVE_BUTTER,
  UPDATE_BUTTER,
  UPDATE_GENIUS,
} from './someConstants';
//In sagas, dispatch is performed by yielding a put effect. 
function* moveButter() {  
  yield put({ type: UPDATE_BUTTER, payload: { hasMoved: true } });
  yield put({ type: UPDATE_GENIUS, payload: true });
}

//Since we're handling the specific dispatches to the store in the saga, we can
//introduce a new action type, MOVE_BUTTER, which the Robot component will
//dispatch. The takeEvery effect calls moveButter whenever MOVE_BUTTER is dispatched.
function* watchMoveButter() {  
  takeEvery(MOVE_BUTTER, moveButter);
}

This neatly solves the major issues with the previous approaches. Since this glue code has no interest or bearing on the structure of the UI directly, there is no longer any need to group the components. Each component is now able to have its own selectors and actions, providing just the data the component needs, and no more than that. What the actions do, once triggered, are of no interest to the component. It's not the component's job to know. Like the butter passing robot, the components have a very specific role - they display data and handle UI interactions (which will nearly always result in an action being dispatched).

This allows for easier creation of composable 'connected' (i.e. connected to redux) components. They're focused on one task, they only utilise props directly relevant to them, and as long as you make sure your action types are easily traced (e.g. via static imports of the type constant for sagas, or importing and dispatching the thunk itself for redux-thunk), it will always be easy enough to find the functionality the action will trigger.

A common example of a saga which would potentially perform many updates would be any form of http request. For example a simple request saga could:

  • Dispatch an action to update a reducer related to the loading status of the app
  • Perform the request
  • If it failed, dispatch an action to a reducer related to global notifications in the app, or call another saga which handles request errors.
  • If it succeeded, dispatch an action to update the store
  • Dispatch an action to update the loading status of the application again.

Of course, all the component did was dispatch the relevant action, it didn't need to know any of the implementation details.

In tl;dr form: You'll probably want to have a 'glue code' layer between your React and Redux layers if you're aiming to perform complex functionality as a result of action dispatches. Sagas are one way of creating this layer.

With the scene set, the next post will be exploring the approach redux sagas take to solving this issue, and my experience in using them so far.

Author image
Kai is a front-end engineer at Onfido, and has definitely never even heard of Rick & Morty.
top