Using Redux-Actions — Why and How?

With a lot of features… comes a lot of code

This cannot be more true for Redux. Redux gives us a powerful way to manage the state of our application. But it also gives us a lot of code to take care of. To create one update, we need to change things across many other files. After a while, the code required to define and handle action creators starts to get a little repetitive.

In this post, we will see how we can use the Redux-Actions Library to cut on the redux code without having to sacrifice any features of Redux.


Getting Started

To explain how to use Redux-Actions, I have created a simple Todo App. You can clone this project in your system using the git clone command from here:

This Todo App is built using React and Redux. I have also added support for Redux DevTools so we can see our state, reducers, and actions that are being deployed.

Once you have cloned this app in your system, install the dependencies using the yarn install command:

$ cd todo-app-actions
$ yarn install // or npm install

Next, we need to start the JSON server. This will launch the db.json file, which is just an empty list of todos.

$ yarn dev-server

Next, we need to start the actual Todo app. You can do this by running the dev script with NPM or Yarn.

$ yarn dev

You will now get the Todo App in your browser at localhost:3000.

Take a look at the db.json file. You will see that it has gained a new entry inside it.

If you open the Redux DevTools in your browser you will be able to see all the actions that get deployed when we interact with the app.


createAction

The createAction function is provided by the redux-actions library and as the name suggests, is used to create actions in a React-Redux app.

createAction function reduces boilerplate code and makes sure that the actions follow the Flux Standard Action structure.

Open the reducer/index.js file and add a new import statement for the createAction function from redux-actions library.

import { createAction } from 'redux-action';

We can now use this function to rewrite the action creators shown below:

export const updateCurrent = createAction (UPDATE_CURRENT);
export const loadTodos = createAction (LOAD_TODOS);
export const addTodo = createAction (ADD_TODO);
export const replaceTodo = createAction (REPLACE_TODO);
export const removeTodo = createAction (REMOVE_TODO);
export const showLoader = () => ({type: SHOW_LOADER, payload: true});
export const hideLoader = () => ({type: HIDE_LOADER, payload: false});

I have re-written the updateCurrent, loadTodos, addTodo, replaceTodo, and removeTodo action creators with the createAction function.

The createAction function is going to take the type and return a new function, which returns a value.

On getting that value, createAction takes the type and the value and returns an object with the type property and the payload property.

I haven’t rewritten the showLoader and hideLoader action creator with the createAction function because they do not accept their payload. They are just hardcoded in.


Modify the Action’s Payload

The redux-actions library contains another function called payloadCreatorthat is used to transform the raw input into data that is properly formatted as we want it to be.

As we saw in the above section, when we pass an action type into the createAction function, a function that accepts our value as the payload is returned back to us.

For the showLoader and hideLoader action creators, we need to pass a second argument into the createAction function. Here, the second argument is going to be a function as shown below:

export const showLoader = createAction(SHOW_LOADER, () => true)
export const hideLoader = createAction(HIDE_LOADER, () => false)

If you take a look at the app in the browser, you will see that everything is working as it is supposed to be.

Any argument that is passed into the action creator will be passed into this function as an argument. Let’s create a new function to capitalize the first letter of the user input.

const capitalize = text => {
return text.split('').reduce((acc, letter, idx) => {
return idx === 0 ? letter.toUpperCase() : `${acc}${letter.toLowerCase()}`
}, ``)
}

This function takes in a string and calls the toUpperCase function on the first letter and the toLowerCase letters on the rest of the letters.

We can then pass in this function as an argument to the updateCurrent action creator.

export const updateCurrent = createAction(UPDATE_CURRENT, capitalize);

Your app will now work something like this:


Create Multiple Redux Actions with createActions

Instead of writing our action creators one by one with the createActionfunction, we can use the createActions function to create multiple action creators at once.

Let’s remove the import statement for createAction function in the reducer/index.js file and replace it with the import statement for createActions function.

import {createActions} from 'redux-actions';

Next, erase all the action creators that we have written in the previous sections and replace them with createActions. It is going to take an object as its first argument. This object is an action map, where I am going to use my action types as keys.

createActions({
UPDATE_CURRENT: capitalize,
SHOW_LOADER: () => true,
HIDE_LOADER: () => false
},
LOAD_TODOS,
ADD_TODO,
REPLACE_TODO,
REMOVE_TODO)

Here, the payload creator function is replace with an identity function that takes in a value and returns it. For action creators like LOAD_TODOS, we just need to pass the action types. This will give us an action creator that accepts the value and makes it the payload.

But we are not done yet. In the browser you will get a big list of not definederrors.

This issue is easily solved. All we need to do is write an export statement that destructures the createActions object.

export const {
updateCurrent,
loadTodos,
addTodo,
replaceTodo,
removeTodo,
showLoader,
hideLoader
} =
createActions(
{
UPDATE_CURRENT: capitalize,
SHOW_LOADER: () => true,
HIDE_LOADER: () => false
},
LOAD_TODOS,
ADD_TODO,
REPLACE_TODO,
REMOVE_TODO
)

This will take care of the errors and the app starts working as it should.


Handle Actions with the handleAction function

The handleAction function is used to create a reducer function to handle a specific action.

Start by importing the handleAction function from the redux-actionslibrary.

import { createActions, handleAction } from 'redux-actions';

You will find the reducers at the bottom of the file:

To see how to use the handleAction function, lets create a new reducer called addTodoReducer.

const addTodoReducer = handleAction(
ADD_TODO,
(state, action) => {
return {
...state,
currentTodo: '',
todos: state.todos.concat(action.payload)
}
},
initState
)

In this reducer, I am using the handleAction function to create a reducer for the ADD_TODO action. This action is handled by the reducer function that is passed in as the second argument of handleAction. This function has taken in the state and action and returns the updated state. Finally the initStateis taken in as the third argument.

All that is left to do now is return a call to addTodoReducer in the main reducer function inside the ADD_TODO‘s case.

export default (state = initState, action) => {
switch(action.type) {
case ADD_TODO:
return addTodoReducer(state, action)
case LOAD_TODOS:
return { ...state, todos: action.payload }
case UPDATE_CURRENT:
return { ...state, currentTodo: action.payload }
case REPLACE_TODO:
return {
...state,
todos: state.todos.map(
t => (t.id === action.payload.id ? action.payload : t)
)
case REMOVE_TODO:
return {
...state,
todos: state.todos.filter (t => t.id !== action.payload),
};
case SHOW_LOADER:
case HIDE_LOADER:
return {...state, isLoading: action.payload};
default:
return state;
}
}

All we have done here is remove the return statement from the ADD_TODO case and move it into the addTodoReducer. If you take a look at the browser, you will see that everything works as it is.

As an exercise, follow the instructions in this section and create a separate reducer and call it within the main reducer.


Combine the reducer functions with reduce-reducers

The reduce-reducers can be used with the handleAction function to define separate reducer functions for each action and reduce them into a single reducer function.

In the reducer/index.js file, we have a single main reducer function as shown below:

export default (state = initState, action) => {
switch (action.type) {
case ADD_TODO:
return addTodoReducer (state, action);
case LOAD_TODOS:
return loadTodosReducer (state, action);
case UPDATE_CURRENT:
return updateCurrentReducer (state, action);
case REPLACE_TODO:
return replaceTodoReducer (state, action);
case REMOVE_TODO:
return removeTodoReducer (state, action);
case SHOW_LOADER:
return showLoaderReducer (state, action);
case HIDE_LOADER:
return hideLoaderReducer (state, action);
default:
return state;
}
};

It would be awesome if we could get rid of the switch statement and create separate reducers for each action. To do so, we can use the reduce-reducerslibrary. Let’s install this package into our project.

$ yarn add reduce-reducers

We then need to import it in the reducer/index.js file.

import reduceReducers from 'reduce-reducers';

We can also get rid of the main reducer function. Instead we can write a new export default statement using reduceReducers as shown below.

export default reduceReducers (
addTodoReducer,
loadTodosReducer,
updateCurrentReducer,
replaceTodoReducer,
removeTodoReducer,
showLoaderReducer,
hideLoaderReducer
);

That’s it! Refresh you browser and you will see everything is still the same.


Handle Multiple Actions with combineActions

We can use multiple actions to update the app’s state using the same function. combineActions function is given to us by redux-actions to handle related actions in a single action handler.

We have two reducers named SHOW_LOADER and HIDE_LOADER that are doing the same thing.

We can use reduc-actions to combine these into a single reducer. First, import the combineActions function from redux-actions library.

import { createActions, handleAction, combineActions } from 'redux-actions;

Then, delete the code for showLoaderReducer and hideLoaderReducer and create a new reducer called loaderReducer that uses the combineActionsfunction to combine the SHOW_LOADER and HIDE_LOADER action creators.

const loaderReducer = handleAction(
combineActions(SHOW_LOADER, HIDE_LOADER),
(state, action) => {
return {...state, isLoading: action.payload}
},
initState
)

We also need to add this combined reducer to the export default statement at the bottom. We can remove the hideLoaderReducer and showLoaderReducer as we are no longer using them.

export default reduceReducers (
addTodoReducer,
loadTodosReducer,
updateCurrentReducer,
replaceTodoReducer,
removeTodoReducer,
loaderReducer
);

Handle multiple actions using reducerMap

The handleAction function is used to create a reducer function to handle a specific action. But redux-actions also gives us a function to handle multiple actions. In the reducer/index.js file, import the handleActions function instead of the singular handleAction function.

import { createActions, handleActions, combineActions } from 'redux-actions';

By doing so, we no longer have a need of the export default statement at the bottom of the file. So lets delete it.

Next, we need to define a new export default right before the reducer functions. This new const is going to call the handleActions function that will take an object as its first argument. The object will be known as the reducer map, and it will map all the action types to the reducer functions. Meaning, we will move all the reducers inside this new function as shown below.

export default handleActions (
{
ADD_TODO: (state, action) => {
return {
...state,
currentTodo: '',
todos: state.todos.concat (action.payload),
};
},
LOAD_TODOS: (state, action) => {
return {
...state,
todos: action.payload,
};
},
UPDATE_CURRENT: (state, action) => {
return {
...state,
currentTodo: action.payload,
};
},
REPLACE_TODO: (state, action) => {
return {
...state,
todos: state.todos.map (
t => (t.id === action.payload.id ? action.payload : t)
),
};
},
REMOVE_TODO: (state, action) => {
return {
...state,
todos: state.todos.filter (t => t.id !== action.payload),
};
},
[combineActions (SHOW_LOADER, HIDE_LOADER)]: (state, action) => {
return {...state, isLoading: action.payload};
},
},
initState
);

You can now delete all the reducer functions that we’ve written in the previous section. Refresh the browser to check if the app is still working as it should.


Its a Wrap!

Redux has made data store mutations predictable yet also verbose. With the help of redux-actions, a developer can reduce the boilerplate of a typical React-Redux app, making it easier to read and write our code.

Thanks for reading this post! You can get the final code of this todo app here



Try Bit

Bit is a platform build for the age of code components. It helps developers and teams easily share, develop and sync code between projects to build faster. Give it a try…