# The 'middleware listener' pattern: <br/> better asynchronous actions in Redux For reference, here is a standard **synchronous** Redux action creator: ```js export const simpleAction = () => { return { type: actionTypes.PERFORM_SIMPLE_ACTION }; } ``` Things to note: 1. it does not know anything about how the action is performed 2. it is up to some other part of the application to determine what to do with this action ## Existing async pattern [Redux documentation on async actions](http://redux.js.org/docs/advanced/AsyncActions.html) This is a stripped down example from the Redux documentation: ```js import * as actionTypes from './action-types'; export const fetchPosts = () => { // thunk middleware - allow action creators to return functions return (dispatch) => { dispatch({ type: actionTypes.FETCH_POSTS_REQUEST }); dataService.getPosts() .then(posts => { dispatch({ type: actionTypes.FETCH_POSTS_SUCCESS, posts }); }) .catch(() => { dispatch({ type: actionTypes.FETCH_POSTS_ERROR }); }); }; }; ``` ### Problems 1. The 'action creator' becomes the 'action coordinator' Unlike normal actions, this pattern breaks the idea that the action create function creates an action description. The action creator `fetchPosts` actually coordinates a lot of the action 2. This action must always call `dataService.getPosts()` For this action creator to work `dataService.getPosts()` need to exist and behave in a particular way. This becomes tricky when you want to: - have a different `dataService` for a different consumption points. For example a `mockDataService` This raises the problem of getting the correct `dataService` dependency into the file. Conditional dependencies is a way to solve this problem but it not ideal. - not do anything with the action What if a consumer of one of our components does not have a `dataService` and wants to do something different, or nothing at all with the action? 3. No application level control over the `dataService` The action creator `fetchPosts` does not have access to the store state. It cannot make a decision to not call `dataService.getPosts()` based on the current state. The responsibility for this would be up to the caller of the action creator. ## Middleware async pattern #### What would it look like if we used middleware to help us decompose things? Our action creator becomes beautiful again: ```js // action-creators.js export fetchPosts = () => { return { type: actionTypes.FETCH_POSTS_REQUEST }; }; ``` Middleware ```js // middleware.js import * as actionTypes from './action-types'; const fetchPostsMiddleware = store => next => action => { // let the action go to the reducers next(action); if (action.type !== actionTypes.FETCH_POSTS_REQUEST) { return; } // do some async action setTimeout(() => { // sort of lame because it will actually call // this middleware again store.dispatch({ type: actionTypes.FETCH_POSTS_SUCCESS, posts: [1, 2, 3] }); }); }; ``` ### Good things about this approach - you can have multiple middleware for the same action type, or none to ignore it. - removing responsibility for coordinating the action away from the action creator `fetchPosts` ### Bad things 1. Gives too much power to something that is just responding to actions In middleware you can: - dispatch new actions - control the flow of data to the reducers - perform other powerful tasks This is a lot of power and responsibility given to something that previously had very little power. Notice that the middleware needs to call `next(action);`. This releases the action to the reducers. This would be something that is easy to forget and could cause a lot of pain. 2. Recursive dispatch can be confusing ```js store.dispatch({ type: actionTypes.FETCH_POSTS_SUCCESS, posts: [1, 2, 3] }); ``` This dispatch would fire a new action that would hit the `fetchPostsMiddleware` again! It would not dispatch a second action because of this check: ```js if (action.type !== actionTypes.FETCH_POSTS_REQUEST) { return; } ``` However, it is possible to create infinite loops if you are not careful (I have!). It can also add additional cognitive load to trying to understand how your application works; where as the original pattern is quite simple to reason about. 3. Middleware is synchronous If you did some processing *before* calling `next(action);` you would be adding overhead to every action that goes through your application. ## The 'middleware listener' async pattern #### Taking the power of middleware without the danger We still have a beautiful action creator ```js // action-creators.js export fetchPosts = () => { return { type: actionTypes.FETCH_POSTS_REQUEST }; }; ``` A 'listener' definition ```js // data-service-listener.js import * as actionTypes from './action-types'; import dataService from './data-service'; export default { [actionTypes.FETCH_POSTS_REQUEST]: (action, dispatch, state) => { // in this listener we get some posts // but we could do anything we want dataService.getPosts() .then(posts => { dispatch({ type: actionTypes.FETCH_POSTS_SUCCESS, posts }); }) .catch(() => { dispatch({ type: actionTypes.FETCH_POSTS_ERROR }); }); } // could add other types to this listener if you like }; ``` New middleware to handle listeners ```js // listener-middleware.js export default (...listeners) => store => next => action => { // listeners are provided with a picture of the world // before the action is applied const preActionState = store.getState(); // release the action to reducers before firing // additional actions next(action); // always async setTimeout(() => { // can have multiple listeners listening // against the same action.type listeners.forEach(listener => { if (listener[action.type]) { listener[action.type](action, store.dispatch, preActionState); } }); }); }; ``` Constructing a store ```js // store.js import listenerMiddleware from './listener-middleware'; import dataServiceListener from './data-service-listener'; import reducer from './reducer'; import loggerMiddleware from './logger-middleware'; const store = createStore( reducer, applyMiddleware( // add other middleware as normal loggerMiddleware, // add as many listeners as you like! listenerMiddleware(dataServiceListener) ) ); ``` ### Issues addressed with using middleware > Gives too much power to something that is just responding to actions - Listeners do not need to know anything about `next(action);` or the flow of the application. - Listeners get information and they can do stuff with it only through `dispatch`. - Listeners are **only** called when they are relevant by using `action.TYPE` as a key (eg `[actionTypes.FETCH_POSTS_REQUEST]`) > Recursive dispatch can be confusing Listener does not get called recursively (unless you dispatch an action with a matching key of course) > Middleware is synchronous We made a strong stance that listeners should always be async. This avoids them blocking the reducers and render of your application. Doing this makes it harder for a listener to get itself into trouble. ## Conclusion The 'middleware listener' pattern is a powerful way to take coordination logic out of action creators. It allows different applications to decide what they want to do with particular actions rather than letting the action decide. We already do this with reducers - why not do it with async actions as well!