Reducing boilerplate in Redux Apps with Arc

5,859 reads

@ viniciusdacal Vinicius Dacal Software Engineer, remote worker. Loves creating, sharing and learning.

Redux is awesome! But people often complain about how much boilerplate they have to write when working with it. Part of this problem, is because they feel unproductive defining constants, action creators and big reducers, but also because they don’t have a clear idea on how to organize their project, or even how to proper handle async requests. On this post, we are going to talk about those concerns and present some approaches to handle them.

reactions

About Redux Arc

Arc is a tiny and well tested lib to help you to be productive on your daily work with Redux. It has utilities to abstract the creation of action types, action creators, reducers and it also has an elegant way to manage async requests. It’s also worth mentioning that it has been used for almost a year in production, without regretting a single day!

reactions

First things first, let’s talk about creating actions!

reactions

The process about creating actions

reactions

When we are talking about Redux, usually, the first step we take when we are going to create a new feature is: Create action types and action creators.

reactions

Let’s say we are creating a Contacts app and the first feature we have to implement is the creation of a contact. The type and the action creator for that would look like this:

reactions

export const CREATE = 'CONTACTS_CREATE' ; export const create = ( name, email, phone ) => ({ type : CREATE, payload : { name, email, phone, }, });

Then, you import the action type to use it in your reducer and import the action creator to use in your component.

reactions

The above code is fine, but it’s unlikely that you would have only a creation feature. In a real world, you would have at least a CRUD. So, let’s see the code for that:

reactions

export const CREATE = 'CONTACTS_CREATE' ; export const READ = 'CONTACTS_READ' ; export const UPDATE = 'CONTACTS_UPDATE' ; export const REMOVE = 'CONTACTS_REMOVE' ; export const create = ( name, email, phone ) => ({ type : CREATE, payload : { name, email, phone }, }); export const read = ( id ) => ({ type : READ, meta : { id }, }); export const update = ( id, name, email, phone ) => ({ type : UPDATE, payload : { name, email, phone }, meta : { id }, }); // delete is a reserved word export const remove = ( id ) => ({ type : REMOVE, meta : { id }, });

The above code is very simple, but as you can see, it feels like we are repeating code. If it feels like that in a Contacts CRUD, imagine in an application with dozen of modules.

reactions

If you are familiar with DRY, you know we should avoid code repetition. You may say we are not repeating code, because each creator has it’s own “business logic” and is creating a different kind of action, but I beg to disagree. If you really pay attention, you can see the patterns:

reactions

As we are following Flux Standard Action spec, every time we want to send a content with our action, we should keep it in the action.payload . Any additional meta info, should go under action.meta . If our action indicates an error, action.error should be true and action.payload should contain the actual error.

spec, every time we want to send a content with our action, we should keep it in the . Any additional meta info, should go under . If our action indicates an error, should be true and should contain the actual error. we are always defining actions types and creators with the same name, the only difference is that one is uppercased and the other is camel cased.

Knowing the first pattern, we could try to normalize our creators, changing them slightly to work with the fixed arguments:

payload

meta

reactions

and

Let’s take the create action as an example, instead of having

name

email

phone

payload

reactions

export const CREATE = 'CONTACTS_CREATE' ; export const create = ( payload ) => ({ type : CREATE, payload, });

andas arguments, we could have just the

About the second pattern, the action type could be generated based on the creators name.

reactions

Now that we can normalize our creators and we know that the types could be generated based on creator’s name, it’s completely possible to create a factory that given a config it generates the creators and the types for us.

reactions

A factory with a simple api generating creators and types would be very handy, right? That is exactly what you have on Arc’s

createActions

reactions

require ( "redux" ) const { createActions } = require ( 'redux-arc' ); const { creators, types } = createActions( 'contacts' , { create : null , read : null , update : null , remove : null , });

The function

createActions

reactions

expects a namespace as its first argument and an action definition object as the second.

For the action definition object, each key should be the creator name and each value could be either, an object with the defaults for

payload

meta

error

null

reactions

andorif you don’t want any defaults.

The result will be an object, containing the creators and the types for each action.

reactions

Types

The types from the above config, would look just like the following:

reactions

{ CREATE : 'CONTACTS_CREATE' , READ : 'CONTACTS_READ' , UPDATE : 'CONTACTS_UPDATE' , REMOVE : 'CONTACTS_REMOVE' , }

Notice: the namespace we provided in the config, was used to prefix the action types value, in order to avoid having two different actions with the same name in the application.

reactions

Based on the above object, wherever you would like to use the type CREATE, you could just import the types object and use it such as below:

reactions

import { types } from './actions.js' ; function myReducer ( state, action ) { if (action.type === types.CREATE) { // do something } return state; } types.CREATE // CONTACTS_CREATE

Creators

Based on our CRUD config, the creators object would be similar to this:

reactions

const creators = { create : function ( payload, meta, error ) {...}, read : function ( payload, meta, error ) {...}, update : function ( payload, meta, error ) {...}, remove : function ( payload, meta, error ) {...}, };

Creators creates the actions using the payload, meta and error arguments. They are all optionals, so, you can omit them as you like.

reactions

Take a look at how we would use our create creator:

reactions

import { creators } from './actions.js' const payload = { name : 'Luke' , email : 'luke@jedimasters.co' , phone : '421 421 421' }; creators.create(payload); /* { type: 'CONTACTS_CREATE', payload: { name: 'Luke', email: email: 'luke@jedimasters.co', phone: '421 421 421', } } */

The creators only generates the actions, so, whenever you want to dispatch an action, you have to use it combined with the dispatch method from Redux store, as we are going to see below.

reactions

Using Action Creators in the components

The most common place we use creators is inside a component connected to the store.

reactions

There’s no secret when you have to use a creators generated by arc inside a component. Take a look at the example:

reactions

import React, { Component } from 'react' ; import { connect } from 'react-redux' ; import { creators } from './actions' ; class ContactsForm extends Component { // {...} } const mapStateToProps = ( state ) => ({ /* map your state*/ }); const mapDispatchToProps = ( dispatch ) => ({ create : ( formValues ) => { dispatch(creators.create(formValues)); }, }); export default connect( mapStateToProps, mapDispatchToProps, )(ContactsForm)

As you can see in the above example, we just defined a create method inside mapDispatchToProps, to have access to dispatch method. Then, inside our component we would have access to create through the props.

reactions

this .props.create(formValues);

Creating Reducers

As you already know, we are going to reuse the generated types inside our reducers. But, that are also other bits we have to discuss about reducers.

reactions

Redux calls all the application reducers when we dispatch an action, then, inside the reducers we have to check if the dispatched action means something to the given reducer or not.

reactions

I’ve seen many people using switch cases to handle that situation, where you match the action type with your case and you can use default case to return the previous state. The problem is that switch cases doesn’t scale very well, and you can end up with painful code to maintain.

reactions

I have seen also many people using multiple IFs, including me, but this approach has similar issues with the switch case’s approach.

reactions

With both approaches, you end up dealing with matching logic and state changes in the same place. That way is hard to focus in a small piece of code per time.

reactions

Thinking about that, we created a createReducers function in Arc, which accepts an initial state and a handlers object which the keys are the action types and the values are handlers for each action. Take a look at the example below:

reactions

import { createReducers } from 'redux-arc' ; import { types } from './actions' ; const INITIAL_STATE = []; const onCreate = ( state, action ) => [ ...state, action.payload, ]; const onRead = ( state, action ) => state.find( contact => contact.id === action.meta.id); const onUpdate = ( state, action ) => state.map( contact => contact.id !== action.meta.id ? contact : action.payload ); const onRemove = ( state, action ) => state.filter( contact => contact.id !== action.meta.id); const HANDLERS = { [types.CREATE]: onCreate, [types.READ]: onRead, [types.UPDATE]: onUpdate, [types.REMOVE]: onRemove, }; export default createReducers(INITIAL_STATE, HANDLERS);

The main idea about this approach, is having a handler for each kind of action. That way you can focus in a small chunk of code per time, instead of a function with 100 lines. It also has a better performance in comparison to IFs and switch cases, as it uses short circuit verification. Under the hood its code is similar to the following:

reactions

function createReducers ( initialState, handlers ) { return ( state = initialState, action ) => { const handler = handlers[action.type]; return !handler ? state : handler(state, action); } }

As you can see, there’s no magic in the code, all we are doing is verifying if there’s a handler to the given action. If we do, we just call it providing the state and the action, if we don’t, we just return the previous state. The difference is that you have this abstracted and tested for you, with validation as a bonus, to reduce your debugging time in case you provide an invalid action type or an invalid handler:

reactions

Creating Async Requests

Arc has also a good support to help you handle async requests as it was originally designed to do just that. I plan to explore this topic in another post, but if you are interest on this, take a look at the docs to see how you can use it: redux-arc.js.org/#async-actions.

reactions

Recap

The function createAction accepts a config and returns types and creators.

accepts a config and returns and You can import types and use them with createReducers .

and use them with . createReducers allows you to turn your reducer into small handlers.

allows you to turn your reducer into small handlers. You can import and use the creators inside your components or wherever you need to dispatch an action.

Useful links

reactions

Conclusion

Besides reducing boilerplate and allowing you to split your code elegantly,

createReducers

createActions

reactions

as well asis all about configuration over programmatically implementation. That allows us to run validations to help you with debugging and also allows you to focus on what really meters.

You don’t want to spend time writing boilerplate or debugging your whole application just because you commit a typo.

reactions

You want to implement features and fix bugs as better and faster as it’s possible, without loose flexibility and we want to help you do that!

reactions

Any feedbacks and contributions are very welcome. Feel free to open an issue on github to ask me more about it.

reactions

Did you enjoy the read?

reactions

️❤️ Help us spread the word by giving a like and sharing️️️️ ❤️

reactions

🖖 Don’t forget to follow me, to be notified about future posts! 🖖

reactions

Share this story @ viniciusdacal Vinicius Dacal Read my stories Software Engineer, remote worker. Loves creating, sharing and learning.

Tags