Reduxed — Reduced Redux Boilerplate

Carlos Roberto Gomes Junior
5 min readMar 20, 2019

You can also read this article in Portuguese here.

Currently Redux is one of the best options when we need a state management solution for React applications. However everyone who use Redux know that there is a lot of code boilerplate before we get things working.

It’s always the same thing. For each slice of our application state we usually need:

  • Constants for each action type.
  • Action Creators that return actions.
  • Reducers with many switch cases.
  • Prefixes to avoid action type clashes (eg: {type: “formX/SUBMIT”}).

If you are working in a big or growing application you probably already saw yourself doing writing these things.

Depending on how your application code is organized, you may end up with a lot of files and a structure like the following:

state
├── auth
│ ├── actions.js
│ ├── reducer.js
│ └── types.js
├── dashboard
│ ├── actions.js
│ ├── reducer.js
│ └── types.js
├── registration
│ ├── actions.js
│ ├── reducer.js
│ └── types.js
└── settings
├── actions.js
├── reducer.js
└── types.js

Some time ago Erik Rasmussen wrote a proposal to simplify this, he call it Ducks: Redux Reducer Bundles. The main ideia of this proposal is, instead of split your state logic in many files containing reducers, actions and types constants, you put all together in a single module.

After applying the Ducks pattern our above example will be like this:

state
├── auth.js
├── dashboard.js
├── registration.js
└── settings.js

As everything is related it makes sense to leave everything together. Applying this pattern could reduce the maintenance complexity of your application as it grows.

But even applying the Ducks pattern we still have to write reducers, actions and type constants.

To simplify the code I’ve created the library Reduxed — Reduced Redux Boilerplate who provides a simple way to define your application state.

What is Reduxed?

Reduxed isn't a new state management library to replace Redux. You will use Reduxed with Redux. The ideia is that you define a reducer for each action (without all the switch cases), and with them Reduxed will create all reducer, action creators, and type constants that you need.

How to use Reduxed?

The best way to understand Reduxed is compare with an example where we don't use it. So let's see.

Without Reduxed

// prefix
const prefix = 'app/counter';
// types
export const INCREMENT = `${prefix}/INCREMENT`;
export const DECREMENT = `${prefix}/DECREMENT`;
// action creators
export const increment = (value = 1) =>
({ type: INCREMENT, payload: value });
export const decrement = (value = 1) =>
({ type: DECREMENT, payload: value });
const initialState = 0;// reducer
export const reducer = (state = initialState, action) => {
switch (action.type) {
case INCREMENT:
return state + action.payload;
case DECREMENT:
return state - action.payload;
default:
return state;
}
}

Doing the same with Reduxed

First, import all required functions:

import { 
create,
handler,
getActions,
getReducer,
getTypes
} from "reduxed";

We define the inicial state and some options:

const initialState = 0;
const options = { typePrefix: "app/counter" };

The typePrefix option is used to add a prefix to our actions type.

Using the functions create and handler we define a reducer for each action that we have.

const counter = create(
handler("increment", (state, payload = 1) => state + payload),
handler("decrement", (state, payload = 1) => state - payload)
)(initialState, options); // options are optional

The handler function always expect a name and a reducer function as arguments. The reducer function arguments are the currentstate and the action payload .

We can also write our reducers in a way that we can export them (eg: for testing).

export const increment = (state, payload = 1) => state + payload;
export const decrement = (state, payload = 1) => state - payload;
const counter = create(
handler("increment", increment),
handler("decrement", decrement),
)(initialState, options); // options are optional

The functionsgetReducer , getActions and getTypes are used to get the final reducer (for Redux), action creators and action type constantes.

export const reducer = getReducer(counter);
export const actions = getActions(counter);
export const types = getTypes(counter);

getReducer returns a Redux like reducer function:

reducer(1, { type: 'app/counter/INCREMENT' }) // 2

getActions returns an object like the following:

{
increment: payload => ({
type: 'app/counter/INCREMENT',
payload
}),
decrement: payload => ({
type: 'app/counter/DECREMENT',
payload
}),
}

and getTypes returns an object like the following:

{
increment: 'app/counter/INCREMENT',
decrement: 'app/counter/DECREMENT',
}

The final code:

import { 
create,
handler,
getActions,
getReducer,
getTypes
} from "reduxed";
const initialState = 0;
const options = { typePrefix: "app/counter" };
const counter = create(
handler("increment", (state, payload = 1) => state + payload),
handler("decrement", (state, payload = 1) => state - payload)
)(initialState, options); // options are optional
export const reducer = getReducer(counter);
export const actions = getActions(counter);
export const types = getTypes(counter);

Then, in other application modules we just import what we need:

// state/rootReducer.js
import { combineReducers } from 'redux';
import { reducer as counterReducer } from './counter';
const rootReducer = combineReducers({
counter: counterReducer,
});
// SomeComponent.js
import { actions } from 'state/counter';
// ...export default connect(
mapStateToProps,
actions,
)(SomeComponent);
// someSaga.js
import { types } from './counter';
import { take } from 'redux-saga/effects';
function* someSagaWorker({ payload }) {
yield take(types.increment);
// ...
}

Reusing reducer logic

Sometimes we need a way to reuse our reducers logic, and this isn't a simple task. In Redux docs we can see some some approaches to solve this problem. Reduxed also provides a way to do this.

Suppose that you need many Counter components in the same place, each one with his own Redux state. You can use withScope from Reduxed:

import { combineReducers } from 'redux';
import { withScope } from 'reduxed';
import { reducer } from './counter';

const rootReducer = combineReducers({
counterA: withScope('A', reducer),
counterB: withScope('B', reducer),
counterC: withScope('C', reducer),
});

Then in your mapDispatchToProps you can do something like this:

import { withScope } from 'reduxed';
import { bindActionCreators } from 'redux';
import { actions } from './counter';

const mapDispatchToProps = (dispatch, ownProps) => {
const scopedActions = withScope(ownProps.scope, actions);
return bindActionCreators(scopedActions, dispatch);
}

In your components:

const Counters = () => (
<div>
<Counter scope="A" />
<Counter scope="B" />
<Counter scope="C" />
</div>
);

Conclusion

Reduxed still young, but I already used it with some projects and it really helps making things easier and writing less code. Here you can see a small application where I’ve used it.

If you liked it, or have any questions, or would like to contribute, fell free to comment here, open an issue or a pull request.

And don't forget to try it:
Reduxed — Reduced Redux Boilerplate https://github.com/carlosrberto/reduxed

Thank you!

Note: This is the first article that I wrote in English. I apologize for any English mistakes here. Feel free to comment them too.

--

--

Carlos Roberto Gomes Junior

JavaScript Developer | Functional Programming Lover | Blockchain Enthusiastic