A micro Redux action DSL

There is a part of using Redux that has always tired me: writing actions and action creators. You know those repetitive lines of code you have to write:

export const Actions = {
    DO_SOMETHING: "DO_SOMETHING",
}
export const doSomething = (payload) => ({ type: Actions.DO_SOMETHING, payload })

And the worst is that most of the time you have actions that don't come alone because they are maybe failable. In that case you also have to write those same lines for "DO_SOMETHING_SUCCESS" and "DO_SOMETHING_FAILURE" actions 😞.

The idea I'm tackling here, is to write a micro DSL to help you in this task.

A DSL? What the hell is that?

DSL: Domain Specific Language.

A computer programming language of limited expressiveness focused on a particular domain.Martin Fowler

Ok, but what does it look like?

The DSL rely on small group of functions that generate actions, action creators, and associated actions using action descriptors. Let's have a look at a complete use case:

const Actions = action(onUserProfile("UPDATE_PSEUDO"), failable)

This simple line of code generate for us the following object:

 {
     USER_PROFILE__UPDATE_PSEUDO: (payload = {}) => ({ type, ...payload })
        type: "USER_PROFILE__UPDATE_PSEUDO"
     USER_PROFILE__UPDATE_PSEUDO_SUCCESS: (payload = {}) => ({ type, ...payload })
        type: "USER_PROFILE__UPDATE_PSEUDO_SUCCESS"
     USER_PROFILE__UPDATE_PSEUDO_FAILURE: (payload = {}) => ({ type, ...payload })
        type: "USER_PROFILE__UPDATE_PSEUDO_FAILURE"
}

Which could be use to dispatch actions and test their type in our reducers like below:

store.dispatch(Actions.USER_PROFILE__UPDATE_PSEUDO({ pseudo: "CtrlAltZ" }))

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case Actions.USER_PROFILE__UPDATE_PSEUDO.type:
            return { /* ... */ }
        case Actions.USER_PROFILE__UPDATE_PSEUDO_SUCCESS.type:
            return { /* ... */ }
        case Actions.USER_PROFILE__UPDATE_PSEUDO_FAILURE.type:
            return { /* ... */ }
        default:
            return state
    }
}

Behind the scene

The core of our micro DSL is composed of 2 functions:

  1. action() generate an object containing action, action creator and enhance it using action descriptors
  2. on() allow us to create action scope to enforce action naming conventions
const action = (type, ...descriptors) => {
    const actionCreator = (payload = {}) => ({ type, ...payload })
    actionCreator.type = type
    return {
        [type]: actionCreator,
        ...descriptors.reduce(
            (actions, descriptor) => ({ ...actions, ...descriptor(type) }),
            {}
        ),
    }
}
const on = (scope) => (type) => `${scope}__${type}`

With this couple of functions you we are able to customize the DSL for concrete cases. Starting with some usefull action descriptors:

const failable = (type) => ({
    ...action(`${type}_FAILURE`),
    ...action(`${type}_SUCCESS`),
})
const optimistic = (type) => ({
    ...action(type, failable),
    ...action(`${type}_OPTIMISTICALLY`),
})
const persistable = (type) => action(`${type}_PERSIST`)

And some really context specific scopes:

const onPost = on("POST")
const onUserProfile = on("USER_PROFILE")

To end with clean and readable action.js file(s) like this:

export const Actions = {
    ...action(onUserProfile("UPDATE_PSEUDO"), failable),
    ...action(onUserProfile("UPDATE_AVATAR"), optimistic, persistable),
    // ...
}

Awesome❗️

Unfortunately not 😢

There is a major downside with the current implentation of this action DSL, code-completion (in reducers and action dispatch) doesn't work because of the use of expando pattern and computed property name.

It certainly depend on the IDE you are using but all I have tested don't auto-complete Actions. . This is due to how code analysis is performed.
Using static analysis rather than an execution engine make certain JavaScript pattern hard to detect (for information of the differences, see here).

If you have any idea to fix code-completion feel free to open an issue.