Uncle Bob’s seminal Clean Architecture post has been an inspiration for many software designers and has become increasingly relevant as JavaScript allows business logic code to be reused. There never was an excuse for spreading business logic out all over a code base, but there is even less of one now that we can npm publish core-logic
and then npm install --save core-logic
wherever we need it.
This article takes as its point of departure the Use Case as defined by Uncle Bob:
The software in this layer contains application specific business rules. It encapsulates and implements all of the use cases of the system. These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their enterprise wide business rules to achieve the goals of the use case.
Use cases are often described as:
However, software is messy, abstractions are leaky and often times, if you’re like me, you find yourself dreaming up leviathan Use Cases that spin out of control or trivial Use Cases whose logic you’ve already coded into the UI and that you won’t be removing anytime soon.
What I’d like to present below is some low-hanging fruit for Uncle-Bobbing your Redux app with Middleware. Redux is a simple JavaScript state container that has become a pattern unto itself that is reproduced in libraries for Kotlin and Swift. I strongly recommend you use it — introducing it into your app is trivial and after ten minutes of working with a redux state you’ll fall in love and never turn back.
As a motivating example, I’ll use the Meeshkan Log In screen that actually applies this pattern. As a side note, the whole app applies this pattern, so feel free to download it, poke around and reach out to us if you want to know how certain things are done.
Let’s start with a simplified version of this log in screen:
// login.jsimport React from 'react';import { reduxForm, Field } from 'redux-form';import { connect } from 'react-redux';import { Button, MyTextComponent } from 'ui-lib';import { Text, Alert } from 'react-native';import Actions from 'react-native-router-flux';import analytics from 'analytics-lib';import { login } from 'kludgy-implementation';
const onSuccess = () => {analytics.report('successfully logged in');Actions.main(); // this is our navigation action}
const onFailure = () => {analytics.report('log in error');Alert.alert("Sorry...", "Something went wrong");}
const submitter = ({email, password}) =>login(email, password, onSuccess, onFailure);
const LogIn = ({handleSubmit}) => (<View><Field component={MyTextComponent} name="email" /><Field component={MyTextComponent} name="password" /><Button onPress={handleSubmit(submitter)}><Text>Log In</Text></Button></View>);
export default connect()(reduxForm({form: 'LogIn'})(LogIn));
There are several problems with this implementation:
login
function must bubble up to the UI to accept success and failure callbacks. What if we want to change how many steps are in the log in process or add more options than success and failure? Refactoring doom…Let’s rewrite the above example using middleware. To start, I’ll show our new and improved component straightaway:
// login.jsimport React from 'react';import { reduxForm, Field } from 'redux-form';import { connect } from 'react-redux';import { Button, MyTextComponent } from 'ui-lib';import { Text} from 'react-native';import { loginAction } from 'better-implementation';import Ize, { navSuccessIze, alertIze, alertFailureIze } from 'ize';
const login = Ize(loginAction,navSuccessIze('main'),analyticsIze(),alertFailureIze("Sorry...", Something went wrong"));
const LogIn = ({handleSubmit, login}) => (<View><Field component={MyTextComponent} name="email" /><Field component={MyTextComponent} name="password" /><Button onPress={handleSubmit(login)}><Text>Log In</Text></Button></View>);
export default connect(null, {login})(reduxForm({form: 'LogIn'})(LogIn));
Some reasons to celebrate:
onSuccess
and onFailure
callbacks that we can’t unit test without triggering all sorts of side effects!Ok, but how do we get there through middleware?
We use [redux-ize](https://www.npmjs.com/package/redux-ize)
to implement the Action Creator Creator pattern. Basically, all of that Ize
stuff takes an action and adds a bunch of useful metadata for middleware.
Check out [redux-saga](https://redux-saga.js.org/)
to see how one may handle our async log in call and dispatch success or failure events. As an example:
import { call, put } from 'redux-saga';import { loginSuccessAction, loginFailureAction } from 'actions';import Ize, { navIze, alertIze } from 'ize';
function* logInSideEffect({payload: {email, password},meta: {navSuccess, alertFailure}}) {try {call(login, email, password);put(Ize(loginSuccessAction, navIze(navSuccess)));} catch(e) {put(Ize(loginFailureAction, alertIze(alertFailure)));}}
Note how we use the ize
pattern again here to move navSuccess
and alertFailure
information to a normal nav
and alert
track. This will make sure it is picked up by the middleware when the success or failure action is dispatched.
Easy like Sunday morning…
// analytics.jsimport analytics from 'my-awesome-analytics-provider';
export default store => next => action => {action.meta && action.meta.analytics && analytics(action.type);next(action);}
// nav.jsimport Actions from 'react-native-router-flux';
export default store => next => action => {action.meta && action.meta.nav && Actions[action.meta.nav]();next(action);}
// alert.jsimport Alert from 'react-native';
export default store => next => action => {action.meta &&action.meta.alert &&Alert.alert(...action.meta.alert);next(action);}
We have even more reasons to celebrate!
sinon
stub in one place.Redux markets itself as “a predictable state container for JavaScript apps.” This is true, but one thing you’ll notice is that I haven’t mentioned a single reducer that propagates these actions to a state. Of course, these actions could be tied to a state, and in practice they usually are, but I wanted to keep this simple so that you see how powerful middleware is.
It’s easy to start using Redux Middleware (or any middleware, like composed Rx object transformations) to handle things like navigation, analytics and alerts. Your app more predictable, less work to code, easier to test, more human-readable and, most importantly, squeaky Clean. Who doesn’t want that? Thanks Uncle Bob!