Redux Clerk: Reusable action creators and reducers for async CRUD

A large portion of the interfaces we deal with at Ambassador are dashboards and tables. This means one of our primary tasks is data management, with most logic fitting into one of four common actions: the classic create, read, update, and delete (CRUD). Since these are some of the easiest things we do as engineers, we set out to find a way to speed up the process. This enables us to focus on more difficult tasks like our core business logic. Having slogged through a seemingly endless amount of Redux boilerplate for CRUD tasks and having found limitations in other open source CRUD abstraction libraries, we eventually created Redux Clerk to solve the problem.

If you’ve ever used Redux and Redux Thunk you’re well aware of the repetitive nature of CRUD actions and reducers. Each CRUD operation is made up of the same set of action creators and reducer to handle each of these actions. After we built several sets of these we realized they were almost identical, with the exception of the action names and some async request configuration. Redux Clerk solves the code duplication problem by generating standard actions and reducers needed for CRUD operations.

Before Redux Clerk

// User actions before Redux Clerk
export const createUser = (user) => { ... }
export const fetchUser = (userId) => { ... }
export const updateUser = (user) => { ... }
export const deleteUser = (userId) => { ... }
// User reducer before Redux Clerk
function user(state, action) {
switch(action.type) {
case 'CREATE_USER'
...
case 'CREATE_USER_SUCCESS'
...
case 'CREATE_USER_ERROR'
...
case 'FETCH_USER'
...
case 'FETCH_USER_SUCCESS'
...
case 'FETCH_USER_ERROR'
...
case 'UPDATE_USER'
...
case 'UPDATE_USER_SUCCESS'
...
case 'UPDATE_USER_ERROR'
...
case 'REMOVE_USER'
...
case 'REMOVE_USER_SUCCESS'
...
case 'REMOVE_USER_ERROR'
...
default
return state
}
}

After Redux Clerk

// User actions after Redux Clerk
import { actions } from 'redux-clerk'

export actions({ // user actions config })
// User reducer after Redux Clerk
import { reducer } from 'redux-clerk'

export reducer({ // user reducer config })

Minimal state and derived data

As we built out more sections of our app using Redux we came across the scenario where several sections needed different lists of the same data. In one section of the app a list is displayed in a data table. In another, a detail page shows information about a single list item and other sections contain select fields or typeaheads for choosing items from a list. If we were to build out separate actions and reducers for each of these areas, we would have duplicate data in our Redux store.

Redux Clerk solves this problem by allowing sets of similar data to be created, referred to as datasets. All raw data is stored once based on a unique id. From there multiple datasets can be created which are stored as arrays of unique ids. Redux Clerk then provides a selector which re-computes the derived data from the array of ids.

This is one (of two) features that sets Redux Clerk apart from similar Redux CRUD abstraction libraries. Other libraries we’ve explored provide a reducer that only manages one set of data.

Before Redux Clerk

Before Redux Clerk was implemented, our store had duplicate data.

// Campaign Table Data
[
{ uid: 123, name: 'Campaign 1' },
{ uid: 234, name: 'Campaign 2' },
{ uid: 354, name: 'Campaign 3' }
]

// Campaign Typeahead
[
{ uid: 456, name: 'Campaign 4' },
{ uid: 234, name: 'Campaign 2' },
{ uid: 354, name: 'Campaign 3' }
]

// Campaign Search Results
[
{ uid: 234, name: 'Campaign 2' },
{ uid: 345, name: 'Campaign 3' },
{ uid: 123, name: 'Campaign 1' }
]

// Campaign Detail View
[
{ uid: 234, name: 'Campaign 2' }
]

After Redux Clerk

After Redux Clerk was implemented, our store no longer contained duplicate data.

{
// Full data objects are only stored once and never duplicated.
raw: {
123: { uid: 123, name: 'Campaign 1' },
234: { uid: 234, name: 'Campaign 2' },
345: { uid: 345, name: 'Campaign 3' },
345: { uid: 456, name: 'Campaign 4' }
},

// Redux Clerk stores datasets as arrays of unique ids.
instances: {
campaignTable: [123, 234, 345],
campaignTypeahead: [456, 234, 345],
campaignSearch: [234, 345, 123],
campaignDetail: [234]
}
}

Async actions and wiring up a RESTful API

Ambassador’s backend provides a RESTful API which the frontend uses for all CRUD operations. Whenever there is a CRUD action fired on the frontend, there is usually an asynchronous request that follows. Our goal was to build asynchronous functionality into Redux Clerk without making assumptions about backend interface or data structure.

To provide this functionality, Redux Clerk allows a creator, fetcher, updater and remover to be configured. Each of these are callbacks that are fired when the related action is called. Each callback is provided all relevant data needed to make the request as well as success and error methods which are used to send the results back to Redux Clerk.

This is the second feature that sets Redux Clerk apart. Other libraries we’ve explored require more boilerplate logic to be added in order to support async actions. Redux Clerk takes care of it for you.

When the fetch action is called, Redux Clerk will dispatch the start action, then call the configured fetcher. The fetcher will receive the original params object, sent to the fetch action, as well as callbacks to handle success or failure of the request. Async requests can be made inside the fetcher. Once complete, the provided success or error callbacks can be called accordingly.

Handling optimistic updates

Redux Clerk is also able to update our store optimistically. That means whenever a CRUD action is fired the store is updated instantly and the async request is made in the background. Handling optimistic updates can be tedious on a case-by-case basis. Redux Clerk makes this simple by keeping references to items pending update and automatically rolls back the changes if the asynchronous request fails.

Concluding thoughts

Redux Clerk has helped Ambassador speed up the development process as well as reduce code and data duplication. We hope others use Redux Clerk to make the same improvements in their own apps. Check out the project on GitHub. We welcome any feedback and are accepting PRs to help improve the project!

PS. Join us at the Embassy, we’re hiring!