Keep Your UI Clean With Redux Middleware

Written by meeshkan | Published 2017/11/13
Tech Story Tags: react-native | redux | middleware | meeshkan | ui

TLDRvia the TL;DR App

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.

Motivating example — A Log In Screen

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:

  • Our 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…
  • Ditto for our analytics function, and even worse, we now have to test that every component calls it correctly. What a mess!
  • We have to remember the order of success and failure and why/how it matters, sprinkling logic all over the UI without any spec that tells us why this is the case.
  • The day that we want to change navigation, analytics or alert libraries we are in for a week of refactoring every UI component. Hello subtle code breakage.

Middleware to the rescue!

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:

  • Gone are the explicit calls to libraries like navigation or analytics!
  • Gone is spreading logic all over onSuccess and onFailure callbacks that we can’t unit test without triggering all sorts of side effects!
  • This is way shorter!
  • This is way easier to read!
  • This is way easier to test!

Ok, but how do we get there through middleware?

1. Using Action Creator Creators via the Ize pattern

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.

2. Use a library like redux saga to handle async calls

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.

3. Create some middleware to handle analytics, navigation and alerts

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!

  • Want to change your analytics provider? No prob — two lines of code.
  • Want to test your middleware? No prob — use a sinon stub in one place.
  • Want to kill off certain navigations or alerts depending on the app’s state? You have access to the store now!
  • Want to test the order of all this stuff? No prob — just write one test to verify your redux store configuration.

Summing it all up — Redux is More than a State Container

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!


Published by HackerNoon on 2017/11/13