Logging Redux Dispatch Calls Using Middleware And Decorators In AngularJS
Yesterday, I finally finished my first attempt at learning about Redux by Dan Abramov. I still don't feel very confident with it; but, at least I was able to get something working in my AngularJS application. I do have a gut feeling that using a centralized Store to manage state is a good idea and simplifies data synchronization. But, I don't know how I feel about some of the non-core features of Redux. Specifically, I'm talking about the use of middleware to implement what "feels to me" like non-Redux responsibilities. To explore this feeling, I wanted to look at how you can log Redux .dispatch() calls using either custom middleware or decorators in AngularJS.
Run this demo in my JavaScript Demos project on GitHub.
I'm new to the world of Flux, so what I'm thinking here might just be crazy bananas. But, the reason that middleware seems odd to me is because Redux (and any other Flux implementation) feels more like a library and less like a framework. As such, it seems like it should defer middleware responsibilities to something outside of its own code. It also seems odd that middleware can fundamentally change the synchronous nature of actions as they are intercepted within the middleware dispatch chain.
But, this is just my personal opinion. And, to be fair, I'm new to Redux and have never tried using middleware. So, I wanted to take a look at logging dispatch() calls, in an AngularJS application, using both Redux middleware as well as an AngularJS decorator.
In the following demo, I'm incrementing a counter. As the "INCREMENT" action is dispatched, I'm logging the action object to the console.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Logging Redux Dispatch Calls Using Middleware And Decorators In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController as vm">
<h1>
Logging Redux Dispatch Calls Using Middleware And Decorators In AngularJS
</h1>
<p>
<a ng-click="vm.increment()">Increment value</a>: {{ vm.counter }}
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/redux/redux-3.0.5.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.7.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
angular.module( "Demo", [] );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide the single Redux store for the entire application.
angular.module( "Demo" ).factory(
"store",
function storeFactory( Redux, $log ) {
// Create the store enhancer that will apply the logging middleware.
// --
// NOTE: If I were going to use middleware in an actual app, I think
// that I would author the store as a "provider" and then make the
// middleware configurable during the config phase (much like HTTP
// interceptors can be defined during the config phase). This way,
// the store factory would only be concerned with creating the store
// and not with defining the logic of the middleware (maybe a good
// follow-up post, eh?).
var storeEnhancer = Redux.applyMiddleware( loggingMiddlewareFactory );
// Create the enhanced createStore() method. The resulting function
// acts like the .createStore() method, but creates a store with the
// desired middleware applied.
var enhancedCreateStore = storeEnhancer( Redux.createStore );
// Create the enhanced Redux store.
var store = enhancedCreateStore(
function rootReducer( state, action ) {
// If the state is undefined, return the default structure. When
// Redux is initializing the store, it dispatches an INIT event
// with an undefined state.
if ( ! state ) {
return({
counter: 0
});
}
switch ( action.type ) {
case "INCREMENT":
state.counter += action.payload.delta;
break;
}
return( state );
}
);
return( store );
// ---
// PRIVATE METHODS.
// ---
// I provide logging middleware for the given store.
// --
// NOTE: This is only ever called once per store.
function loggingMiddlewareFactory( store ) {
return( dispatchFactory );
// I generate the dispatch interceptor for the middleware.
// --
// NOTE: This is only ever called once per store.
function dispatchFactory( nextDispatch ) {
return( dispatch );
// I log the given action before passing the action onto the next
// dispatcher in the middleware chain.
// --
// NOTE: This is called once per dispatch life-cycle.
// --
// CAUTION: The nextDispatch() method does not necessarily have
// to be called in a synchronous manner, per say, depending on
// how comfortable you are with Redux shifting from a synchronous
// to an asynchronous workflow (personally, I prefer synchronous).
function dispatch( action ) {
$log.info( "Logging from middleware:", action );
return( nextDispatch( action ) );
}
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I decorate the Redux store, adding logging for the dispatch requests.
angular.module( "Demo" ).decorator(
"store",
function storeDecoratorForLogging( $delegate, $log ) {
// Get a reference to the core dispatch() method since we are about to
// overwrite it with our logging proxy.
var coreDispatch = $delegate.dispatch;
// Overwrite the core dispatch method with our proxy that will
// log-out the current action before dispatching the action to the
// core dispatch method.
$delegate.dispatch = function dispatchProxy( action ) {
$log.info( "Logging from decorator:", action );
return( coreDispatch.apply( $delegate, arguments ) );
};
// Return the decorated store.
return( $delegate );
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I control the root of the application.
angular.module( "Demo" ).controller(
"AppController",
function AppController( $scope, $log, store ) {
var vm = this;
// Subscribe to the store so we can re-render when the store is updated.
store.subscribe( renderState );
// Apply the current state to the view-model.
renderState();
// Expose public methods.
vm.increment = increment;
// ---
// PUBLIC METHODS.
// ---
// I increment the counter.
function increment() {
var action = store.dispatch({
type: "INCREMENT",
payload: {
delta: 1
}
});
$log.info( "Done dispatching:", action );
$log.log( "- - - - - - - - - - - - - " );
}
// ---
// PRIVATE METHODS.
// ---
// I apply the current state to the local view-model.
function renderState() {
vm.counter = store.getState().counter;
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I expose Redux as an injectable.
angular.module( "Demo" ).factory(
"Redux",
function ReduxFactory( $window ) {
var Redux = $window.Redux;
// Remove from the global scope to prevent reference leakage.
delete( $window.Redux );
return( Redux );
}
);
</script>
</body>
</html>
Now, I know that I'm very familiar with AngularJS and that I'm very new to Redux; but, when I look at this code, I find it much easier to reason about the AngularJS decorator because the mechanics of it are very simple and explicit. The Redux middleware, on the other hand, is still a bit confusing to me even after I got it working. For example, why does the Redux middleware require nested factories? Why not a single factory that accepts both the store and the nextDispatch() function? Not to mention that the invokation of the Redux.createStore() method is, itself, proxied and hidden away. The middleware feels a little too "magical" to me.
The thing that made me really unsure of myself, when defining the middleware, was that I had a really hard time trying to name the factory methods. In the Redux documentation, and in many middleware examples, these intermediary functions are just anonymous functions that return another value. But, I like using an explicit name because it forces me to codify what the function is actually doing - it forced me to understand its intent. And, I had trouble doing that.
That said, both of these logging techniques work. And, we run the above code and increment the value a few times, we get the following page output:
As you can see, both the middleware and the decorator approaches to logging work well in an AngularJS application. It's just a matter of which approach you feel more comfortable using. Personally, I like the simplicity and the explicitness of the decorator and the way it keeps responsibilities divided. But, if you are used to using middleware, the Redux.applyMiddleware() approach might feel more natural to you.
Want to use code from this post? Check out the license.
Reader Comments
@All,
After I wrote this, I felt like I came out too strongly in my feelings about Redux middleware. I started to realize that Redux middleware is really no different from something like the AngularJS $http interceptors, which I do like. I tried to take some time and reflect on my feelings:
www.bennadel.com/blog/2990-angularjs-http-interceptors-vs-redux-middleware---why-the-disconnect.htm
Some of it was FUD (fear, uncertainty, doubt); but, I do genuinely think that some use-cases of the Redux middleware is an overreach of responsibilities.