Keducer — Automate writing redux reducers with 5 lines of javascript

Written by shivekkhurana | Published 2017/11/18
Tech Story Tags: redux | react | javascript

TLDRvia the TL;DR App

Photo by Alex Knight on Unsplash

I love working with React and am amazed at the evolution of this frontend paradigm. React and Redux are two brilliant pieces of technology. Any amount of praise will fall short for them.

But one question that kept coming to me was, am I being enough DRY ?

Coming from the world of vanilla flux, Redux felt succinct. But a few big projects later, I was doing the same thing, over and over again. One part of the entire cycle which felt particularly verbose was writing reducers. We take an action and a payload and emit a mutation.

I first wanted to eliminate the process of writing reducers, so I came up with Mutex — A flux without side effects but never finished it because JayStack came up with a similar library called Repatch.

Looking back, Repatch’s idea was on the same line, but extremely mature. If I had worked on Mutex for about 2 years, I would have got Repatch. And being the lazy programmer I’m, it was best to just drop my own lib and use Repatch instead.

But then came the realization of legacy code (and mental models)

I spent a lot of time tweaking the Fractal — A react app structure for infinite scale, and it was dependent on raw Redux. Although, Repatch was inline with my philosophy, I didn’t feel comfortable betting on it because :

  • Tightly coupled actions and reducers made it hard to write automated tests.
  • Working on Repatch meant there is no going back. If I build an application and it doesn’t feel right after sometime, I am stuck.

There were other notable projects like redux-zero, but the second point still held true.

What I wanted

Ideally, I wanted a system that :

  • Fits well in our Fractal pattern
  • Doesn’t bloat the build
  • Reduces (or completely eliminates) the need to write reducers
  • Doesn’t affect the testability of the store
  • Cost of switch should be minimal

The Eureka !

While bootstrapping a new project and copy pasting my usual authentication modules, I observed that most of my reducers :

  • only took the payload and slapped them onto my state
  • and reducer was a function that I define (with a big ass switch case)

And I thought of writing a function, which generates a default reducer that slaps the payload onto the state. It’s called Keducer (“Kay-Deu-Cer”; and it’s just 5 lines of code that has eliminated 95% of my reducers).

I so wanted to build an npm module for this but felt like it would be best to not contribute more to js fatigue, so a gist is all I made.

How Redux works without keducers

In a general Redux setting, here’s what a typical login reducer looks like :

export default function reducer(state={}, action) {switch (action.type) {case 'auth.loginError':return {...state, loginErrors: action.payload.errors};

case 'auth.loginSuccess':  
  return {...state, isUserLoggedIn: true};

default:  
    return state;  

}}

And the action definition might be on the lines of :

export function login(email, password) {return (dispatch) => {api.post('/auth/login', {email, password}).then((res) => {Promise.all([storage.setItem('token', res.token),storage.setItem('user', JSON.stringify(res.user)),]).then(() => {dispatch({type: 'auth.loginSuccess'});});}).catch((err) => {dispatch({type: 'auth.loginError',payload: {errors: Object.values(err.data)}});});};}

You simply load this reducer on a store and `Provide` it to the root component and then dispatch(login) function on click events.

How Redux works with keducers

With a keducer, whatever you pass as a payload is sticked on top of your state. But it does expect your actions to be . (dot) separated strings so you might have to do some work there.

Step one : Setup a module

Module is basically a ducks structure term for actions + reducers in one file. You can read more about it in my Fractal Structure article.

The only notable difference is the absence of a reducer and export of a default keducer instead. Note that a keducer takes a key named auth that basically tells it to listen to all actions which have an auth. prefix.

Now keducer will automatically take your payload and append it to state under the auth tree.

import config from 'config';import api from 'utils/api';import storage from 'utils/localStoragePromise';import keducer from 'utils/keducer';

export default keducer('auth'); // <---- Where the magic happens

export function login(email, password) {return (dispatch) => {api.post('/auth/login', {email, password}).then(res => {Promise.all([storage.setItem('token', res.token),storage.setItem('user', JSON.stringify(res.user)),]);dispatch({type: 'auth.loginSuccess', payload: {token, user}});}).catch(err => {dispatch({type: 'auth.loginError',payload: {loginErrors: err.data.err}});});};}

Step 2 : Setup your root store

Because keducer returns a function, there is nothing new that you need to do here. Just combineReducers as normal and pass it to the Provider.

const rootReducer = combineReducers({auth,});

const store = createStore(rootReducer,compose(applyMiddleware(thunk),window.devToolsExtension ? window.devToolsExtension() : f => f));

This should be enough to get started, but if you are a Redux power user, you might be thinking :

  • “Huh, this kills the entire concept of Redux because you can’t possibly share actions among various reducers” and
  • “You can’t even update the state if it’s dependant on a previous state (for example loading more items in a list when user scrolls to the bottom of a page)”.

Don’t worry power user, keducer has you covered.

Step 3 : Access old state or share actions among keducers

The keducer function takes a second argument, which is a map like :

export default keducer('users', {'auth.loginSuccess': (state, payload) => ({...state, ...payload, ...{customStuff: true}})})

The keys of this map are action types that you wish to capture and the values are functions which get the state and the payload as a parameter. This state is the local reducer state, not the app state, so you can expect to do all your normal redux sorcery if you wish too (but you rarely would).

A demo project

I’m working on one and will have one soon. The soon is relative to the amount of claps and views this article gets.

Closing thoughts

These 5 lines of code save a lot of time writing reducers, but the bigger point here is that someone like me (without a lot of experience) was able to come up with something so obvious, whereas the entire js community bets on the verbose version.

Also, the competing standards and js fatigue stopped me from building yet another module. I was kinda pissed with the way js is taught, used and improved and it all seems like a fad.

I went searching for better solutions and have found one in clojurescript. My understanding about it is not as mature as js, but it is very very very promising. Very very. It’s like targeting js as a JVM and is backed by Cognitect (the company behind clojure, where Rich Hickey works).

🤙

I’m currently experimenting heavily with it (clojurescript), and please let me know if you want me to write about it.

🙏🏼

Thanks for taking time to read my work. I hope it will be helpful for your projects.

❤️

You might also like to read :


Published by HackerNoon on 2017/11/18