CAUTION: This post is 100% subjective.
To start off, I like the idea of Redux. And, of one-way data flows. I like that Redux provides a predictable state management system. And, I even like the idea of having middleware so that I can perform cross-cutting concerns like logging actions and capturing and reporting errors (the first two middleware examples that Redux outlines in its documentation). Where things go off the rails for me is when people start including asynchronous middleware. The beauty of Redux is its simplicity and its separation of concerns. And, for me, when you start trigging asynchronous workflows inside of Redux middleware, it feels like that separation of concerns is broken.
| || || |
| || |
| || || |
I've had this feeling for a long time, but have never been able to clearly articulate a specific reason that it feels so uncomfortable. Then, last night at Nir Kaufman's presentation on Clean Architecture for Single Page Applications at the React NYC Meetup, I had a moment of clarity:
When you mix asynchronous middleware into your Redux workflow, you run the possibility of dispatching actions that have nothing to do with state management; and, are only dispatched as a means to trigger the asynchronous middleware.
The moment you do that, Redux is no longer a predictable state management tool - it's a runtime platform. The problem is, you already have a runtime platform: the parent application. There's no need to build yet another one inside of it using Redux.
By breaking the separation of concerns and treating Redux as a runtime, you are left without a clear indication of where functionality should live. As a thought experiment, imagine that you have an asynchronous middleware that changes the state of the application and triggers an HTTP request. Now imagine that at some point in the future, that specific state change is no longer needed (ex, the HTTP "activity indicator" is no longer in the user interface). At that point, should the HTTP request stay in the Redux middleware?
If "no" (it should be removed), then why was it there in the first place?
And, if "yes" (it should stay in the Redux middleware), does it then follow that every single HTTP request made by your application should also be triggered inside of Redux middleware? After all, if an HTTP request that has nothing to do with state management can live inside of Redux, then why not include every other HTTP request your application makes?
This lack of clarity points to a missing abstraction: something that orchestrates the asynchronous control-flow and calls into the Redux store as actions need to be dispatched. I don't feel strongly about where that functionality lives. In a one-off case, you could simply keep this orchestration in your "smart components". And, if this asynchronous workflow needs to be consumed by various parts of your application, you could factor it out into a separate Class that can be provided to components as needed.
Remember, you had to do all of this asynchronous stuff before you even had Redux as an option. And, in that time before Redux, the problem wasn't making and managing HTTP requests - the problem was managing state in a predictable way. As such, it makes sense that you should use Redux to manage state and leave asynchronous control-flow where ever it used to live. Keep a clean separation of concerns.
It's funny, my unease with asynchronous middleware goes back for almost three years:
... sometimes it takes a long time to figure out why something feels weird.