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)
);
});