Skip to content

Instantly share code, notes, and snippets.

@superhawk610
Last active August 23, 2018 17:42
Show Gist options
  • Save superhawk610/6b3302ccc423d344ba2d3c024f826363 to your computer and use it in GitHub Desktop.
Save superhawk610/6b3302ccc423d344ba2d3c024f826363 to your computer and use it in GitHub Desktop.
Brief description of why to use redux sagas in place of middlewares

On a basic level, middlewares are responsible for handling side effects of redux actions - they fit, as their name would imply, in the middle of the redux chain. Our setup for Guppy looks something like this:

action -> store
            |
        middleware
            |
        middleware
            |
        middleware
            |
         reducers

Components in the client just watch for changes in the reducers' state using mapStateToProps, but we need some way to respond to redux actions from the host, since the host is the only portion of the app that can actually run arbitrary shell code. The middleware model listens for specific actions and launches appropriate side effects (like launching the webpack dev server, aborting a running task, etc.)

Sagas are essentially identical to middlewares as far as intended usage, but fit into the chain at the end, which is better for our mental model of their purpose since they're only supposed to be used for side-effects, not directly mutating actions (which is something middlewares can do but is generally an anti-pattern). With sagas, the flow becomes:

action -> store
            |
         reducers
            |
         rootSaga --> saga
            ├-------> saga
            └-------> saga

Each existing middleware maps 1-to-1 with its corresponding saga, so much of the refactor was just changing the syntax, not actually writing any novel implementation. Sagas use ES6 generators to mock async/await syntax. A simple saga may look like

import { select } from 'redux-saga/effects';
import { mySelector } from '../reducers';

function* mySaga() {
  const activeElement = yield select(mySelector);
}

which is essentially the same as

import { mySelector } from '../reducers';

const mySaga = async () => {
  const activeElement = await mySelector(state);
}

but it injects state for you. The real reason for this unique syntax is that when you're writing tests, the external calls are never made, you just assert that the call structure is correct, something like:

// mySaga.test.js
import { select } from 'redux-saga/effects';
import { mySaga } from './mySaga';
import { mySelector } from '../reducers';

// ...
it('should call mySelector', () => {
  const saga = mySaga();

  expect(saga.next().value).toEqual(
    select(mySelector)
  );
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment