Skip to content

loklaan/redux-motive

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

52 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Redux Motive stability

size NPM Travis Codecov

Simplify writing action creators, reducers and effects - without breaking redux.

const { reducer, ...actionCreators } = ReduxMotive({
  config: {},
  sync: {
    // Sync function, combines Action Creator and Reducer
    addTodo (state, todo) {
      return assign({}, state, { todos: [ ...state.todos, todo ] })
    },
  },
  async: {
    // Async function, combines Action Creator and Effect
    async createTodo (motive, text, isDone) {
      const todo = await api('/todo', {text, isDone})
      motive.addTodo(todo)
    }
  },
})

Install

yarn add redux-motive

Requirements

Add redux-thunk to your store's middleware. See redux-thunk docs for more details.

yarn add redux-thunk
import thunk from 'redux-thunk'
const store = createStore(reducers, applyMiddleware(thunk))

Preamble

In UI development, our motive's for using redux are predictable.

  1. Reduce an Action to change the state now, to rerender the UI soon.
  2. Reduce the lifecycle of side effects, from an Action, to change state over time, to rerender the UI as the side effects progress.

Redux is great for splitting data-flow concerns into small concepts, but it can introduce indirection to a developers code, and at times this becomes the source of errors.

Motive removes indirection, by combining the purpose of a data-flow function to be both an Action Creator and a Reducer, or an Action Creator and an Effect.

Comparison

Generate action creators and a reducer with Motive.

const { reducer, ...actionCreators } = ReduxMotive({
  sync: {
    // Sync function, combines Action Creator and Reducer
    addTodo (state, todo) {
      return assign({}, state, { todos: [ ...state.todos, todo ] })
    },
  },
  async: {
    // Async function, combines Action Creator and Effect
    async createTodo (motive, text, isDone) {
      const todo = await api('/todo', {text, isDone})
      motive.addTodo(todo)
    }
  },
})

Write action types, action creators and reducers with common redux boilerplate.

const ADD_TODO = '@@MOTIVE/ADD_TODO'
const CREATE_TODO_START = '@@MOTIVE/CREATE_TODO_START'
const CREATE_TODO_END = '@@MOTIVE/CREATE_TODO_END'
const CREATE_TODO_ERROR = '@@MOTIVE/CREATE_TODO_ERROR'

const reducer = (state, action) => {
  switch (action.type) {
    case ADD_TODO:
      return assign({}, state, { todos: [ ...state.todos, todo ] })
    case CREATE_TODO_START:
      return assign({}, state, { progressing: true })
    case CREATE_TODO_END:
      return assign({}, state, { progressing: false })
    case CREATE_TODO_ERROR:
      return assign({}, state, { error: action.payload, progressing: false })
  }
}

const actionCreators = {
  addTodo (todo) {
    return { type: ADD_TODO, payload: { todo } }
  },

  createTodo (text, isDone) {
    return (dispatch) => {
      dispatch({ type: CREATE_TODO_START })
      api('/todo', {text, isDone})
        .then(todo => {
          dispatch(actionCreators.addTodo(todo))
          dispatch({ type: CREATE_TODO_END })
        })
        .catch(err => {
          dispatch({ type: CREATE_TODO_ERROR, payload: err })
        })
    }
  }
}

Summary

Inferring common redux patterns into ReduxMotive allows for less coding.

  • Action Creators often pass their params to Reducers in the Action; ReduxMotive always does behind the scenes.
  • The progress of an effect's lifecycle in ReduxMotive is reduced to state at common stages: start, end or error.
  • Dispatching actions from the end of effects is guaranteed; ReduxMotive provides dispatch-bound Action Creators in an effect's first parameter.

API

ReduxMotive( { config, sync, async } )

The returned object can be used to provide a reducer to the Redux.

Additionally, every function configured for sync and async are accessible as dispatchable Action Creators.

const motive = ReduxMotive({
  config: {}
  sync: {
    todo () {},
  },
  async: {
    async fetchTodo () {}
  }
});

console.log(motive);
// {
//   reducer,               Reducer function, wrapping all configured sync fns
//   todo,                  An Action Creator generated from sync.todo
//   fetchTodo              An Action Creator generated from async.fetchTodo
// }

Configuring

# config

Initial state, default handlers for state/end/error, and optional prefix for action types.

ReduxMotive({
  // Default config values
  config: {
    prefix: '',
    initialState: {},
    handlers: {
      start: (state) => assign({}, state, { progressing: true }),
      end: (state) => assign({}, state, { progressing: false }),
      error: (state, error) => assign({}, state, { progressing: false, error })
    },
  }
})

# sync

A collection of functions that combine the principles of an Action Creator and a Reducer.

They should:

  1. Always return new state
  2. Should not call any "side effects"
const { todo } = ReduxMotive({
  sync: {
    todo (state, isDone) {
      return { ...state, isDone }
    }
  }
})

dispatch( todo(true) )

# async

Combination of an Action Creator and an Effect.

Function that is given a motive Object and any additional arguments from the generated Action Creator.

Expected to dispatch new Actions from invoke side effects (like server API calls).

Should return a Promise. The async function keyword can be used.

motive Object

  • dispatch
  • getState
  • Action Creators returned by ReduxMotive, bound to dispatch
ReduxMotive({
  // ...

  async: {
    async fetchTodo (motive) {
      const todo = await api();
      motive.todo(todo.isDone)
    }
  }
})

Lifecycles for an Async Function

Refer to the Comparison for when 'lifecycle' stages are actioned and reduced.

The stages can be overridden:

  • In the config
  • Per (asynchronous) function
ReduxMotive({
  config: {
    handlers: { /* ... */ }
  },

  async: {
    fetchTodo: {
      handlers: {
        start (state) { /* ... */ },
        end (state) { /* ... */ },
        error (state) { /* ... */ }
      },
      async effect (motive) {
        const todo = await api();
        motive.todo(todo.isDone)
      }
    }
  }
})

Action Types

Action types for each Action Creators are available as properties, which is useful when attempting to match the types in a explicit way.

console.log(motive.todo.ACTION_TYPE)
// @@MOTIVE/<PREFIX>/TODO_SYNC

console.log(motive.fetchTodo.ACTION_TYPE_START)
// @@MOTIVE/<PREFIX>/SYNC_TODO_START
console.log(motive.fetchTodo.ACTION_TYPE_END)
// @@MOTIVE/<PREFIX>/SYNC_TODO_END
console.log(motive.fetchTodo.ACTION_TYPE_ERROR)
// @@MOTIVE/<PREFIX>/SYNC_TODO_ERROR

You don't need to use these if you're dispatching the generated Action Creators.

Alternatives & inspirations

Library Description
redux-schemas Similar redux util library, making different API choices, but with more utility.
freactal Unidirection store for React, with a concise api for async actions and selectors.

License

Licensed under the MIT License, Copyright © 2017-present Lochlan Bunn.

About

Simplify writing action creators, reducers and effects - without breaking redux.

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published