Skip to content

Commit

Permalink
Whole library re-implementation
Browse files Browse the repository at this point in the history
* Features:
  - "redux-actions" compatible actions (FSA)
  - custom separators or stages
* Thanks to https://github.com/afitiskin for the implementation proposal
  • Loading branch information
svagi committed Apr 9, 2018
1 parent a31fcbf commit 7a8ce0d
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 163 deletions.
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ Simple, yet effective tool for removing Redux boilerplate code.
[![CircleCI](https://circleci.com/gh/svagi/redux-routines.svg?style=shield)](https://circleci.com/gh/svagi/redux-routines)
[![Coverage Status](https://coveralls.io/repos/github/svagi/redux-routines/badge.svg)](https://coveralls.io/github/svagi/redux-routines)


## About

The `redux-routines` is utility library for [redux](https://github.com/reactjs/redux) whose main goal is simplicity and boilerplate reduction.
Expand All @@ -17,7 +16,13 @@ The `redux-routines` is utility library for [redux](https://github.com/reactjs/r
npm install --save redux-routines
```

## Features

* Predefined actions creators and action types for common async tasks in a single object called "routine"
* [FSA](https://github.com/acdlite/flux-standard-action) compatible – based on [redux-actions](https://github.com/reduxactions/redux-actions) library

## The gist

```js
import { createStore } from 'redux'
import { createRoutine } from 'redux-routines'
Expand Down Expand Up @@ -54,7 +59,7 @@ const initialState = {
}

// The reducer
function users (state = initialState, action) {
function users(state = initialState, action) {
switch (action.type) {
case fetchUsers.TRIGGER:
return { ...state, isProcessing: true }
Expand All @@ -71,9 +76,7 @@ function users (state = initialState, action) {

// The store
const store = createStore(users)
store.subscribe(() =>
console.log(store.getState())
)
store.subscribe(() => console.log(store.getState()))

// Describe state changes with routine actions
store.dispatch(fetchUsers.trigger())
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,8 @@
"jest": "^20.0.4",
"rimraf": "^2.6.1",
"standard": "^10.0.2"
},
"dependencies": {
"redux-actions": "^2.3.0"
}
}
79 changes: 26 additions & 53 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,37 @@
/**
* redux-routines
*/
const identity = f => f

export const TRIGGER = 'TRIGGER'
export const REQUEST = 'REQUEST'
export const SUCCESS = 'SUCCESS'
export const FAILURE = 'FAILURE'
export const FULFILL = 'FULFILL'
import { createAction } from 'redux-actions'

export function prefixType (prefix, type) {
if (typeof prefix !== 'string') {
throw new Error('Invalid routine prefix. It should be string.')
}
return `${prefix}_${type}`
// Default routine settings
export const DEFAULT_SETTINGS = {
separator: '/',
stages: ['TRIGGER', 'REQUEST', 'SUCCESS', 'FAILURE', 'FULFILL']
}

export function createAction (type, payload, ...args) {
return Object.assign({}, ...args, { type: type, payload: payload })
// Routine action type factory
export function createActionType(prefix, stage, separator) {
if (typeof prefix !== 'string' || typeof stage !== 'string') {
throw new Error('Invalid routine prefix or stage. It should be string.')
}
return `${prefix}${separator}${stage}`
}

export function createRoutine (prefix, enhancer = identity) {
const routine = {
TRIGGER: prefixType(prefix, TRIGGER),
REQUEST: prefixType(prefix, REQUEST),
SUCCESS: prefixType(prefix, SUCCESS),
FAILURE: prefixType(prefix, FAILURE),
FULFILL: prefixType(prefix, FULFILL),
state: {
trigger: false,
request: false,
success: false,
failure: false,
fulfill: false
},
trigger (payload, ...args) {
routine.state.trigger = true
return enhancer(createAction(routine.TRIGGER, payload, ...args))
},
request (payload, ...args) {
routine.state.request = true
return enhancer(createAction(routine.REQUEST, payload, ...args))
},
success (payload, ...args) {
routine.state.success = true
routine.state.failure = false
return enhancer(createAction(routine.SUCCESS, payload, ...args))
},
failure (payload, ...args) {
routine.state.success = false
routine.state.failure = true
return enhancer(createAction(routine.FAILURE, payload, ...args))
},
fulfill (payload, ...args) {
routine.state.fulfill = true
return enhancer(createAction(routine.FULFILL, payload, ...args))
}
// Routine factory
export function createRoutine(prefix, payloadCreator, metaCreator, settings) {
const { stages, separator } = Object.assign({}, DEFAULT_SETTINGS, settings)
const createRoutineAction = stage => {
const type = createActionType(prefix, stage, separator)
return createAction(type, payloadCreator, metaCreator)
}
function call (payload, ...args) {
return routine.trigger(payload, ...args)
}
return Object.assign(call, routine)
return stages.reduce((routine, stage) => {
const actionCreator = createRoutineAction(stage)
return Object.assign(routine, {
[stage.toLowerCase()]: actionCreator,
[stage.toUpperCase()]: actionCreator.toString()
})
}, createRoutineAction(stages[0]))
}

export default createRoutine
167 changes: 64 additions & 103 deletions src/index.test.js
Original file line number Diff line number Diff line change
@@ -1,134 +1,95 @@
/* global describe, it, expect */
import { createAction, createRoutine, prefixType } from './index'
import { createRoutine, createActionType, DEFAULT_SETTINGS } from './index'

const { stages } = DEFAULT_SETTINGS

describe('createRoutine', () => {
it('should be a function', () => {
const routine = createRoutine('test')
expect(typeof routine).toBe('function')
})
it('should have initial state', () => {
const { state } = createRoutine('test')
expect(state.trigger).toBe(false)
expect(state.request).toBe(false)
expect(state.success).toBe(false)
expect(state.failure).toBe(false)
expect(state.fulfill).toBe(false)
})
it('should have trigger state', () => {
const routine = createRoutine('test')
const { state } = routine
expect(routine.trigger().type).toContain('TRIGGER')
expect(state.trigger).toBe(true)
expect(state.request).toBe(false)
expect(state.success).toBe(false)
expect(state.failure).toBe(false)
expect(state.fulfill).toBe(false)
})
it('should have request state', () => {
it('should have all routine properties', () => {
const routine = createRoutine('test')
const { state } = routine
expect(routine.request().type).toContain('REQUEST')
expect(state.trigger).toBe(false)
expect(state.request).toBe(true)
expect(state.success).toBe(false)
expect(state.failure).toBe(false)
expect(state.fulfill).toBe(false)
})
it('should have success state', () => {
const routine = createRoutine('test')
const { state } = routine
expect(routine.success().type).toContain('SUCCESS')
expect(state.trigger).toBe(false)
expect(state.request).toBe(false)
expect(state.success).toBe(true)
expect(state.failure).toBe(false)
expect(state.fulfill).toBe(false)
stages.forEach(stage => {
expect(routine).toHaveProperty(stage.toUpperCase())
expect(routine).toHaveProperty(stage.toLowerCase())
})
})
it('should have failure state', () => {
it('should create all action types with default stages', () => {
const routine = createRoutine('test')
const { state } = routine
expect(routine.failure().type).toContain('FAILURE')
expect(state.trigger).toBe(false)
expect(state.request).toBe(false)
expect(state.success).toBe(false)
expect(state.failure).toBe(true)
expect(state.fulfill).toBe(false)
stages.forEach(stage => {
const actionCreator = routine[stage.toLowerCase()]
const actionType = routine[stage]
expect(actionType).toBe(`test/${stage}`)
expect(String(actionCreator)).toBe(actionType)
})
})
it('should have fulfill state', () => {
it('should create all action creators with default stages', () => {
const routine = createRoutine('test')
const { state } = routine
expect(routine.fulfill().type).toContain('FULFILL')
expect(state.trigger).toBe(false)
expect(state.request).toBe(false)
expect(state.success).toBe(false)
expect(state.failure).toBe(false)
expect(state.fulfill).toBe(true)
})
it('should modify payload with enhancer', () => {
const routine = createRoutine('test', action => {
action.payload += 1
return action
stages.forEach(stage => {
const actionCreator = routine[stage.toLowerCase()]
const actionType = routine[stage]
expect(actionCreator()).toEqual({ type: `test/${stage}` })
expect(actionCreator()).toEqual({ type: actionType })
})
expect(routine.trigger(0).payload).toBe(1)
expect(routine.request(0).payload).toBe(1)
expect(routine.success(0).payload).toBe(1)
expect(routine.failure(0).payload).toBe(1)
expect(routine.fulfill(0).payload).toBe(1)
})
it('should trigger on routine invocation', () => {
const routine = createRoutine('test')
const action = routine()
expect(action.type).toContain('TRIGGER')
expect(routine.state.trigger).toBe(true)
it('should create routine with payloadCreator', () => {
const routine = createRoutine('test', val => val + 1)
stages.forEach(stage => {
const actionCreator = routine[stage.toLowerCase()]
expect(actionCreator(0).payload).toBe(1)
})
})
})

describe('createAction', () => {
it('should create an action object', () => {
const type = 'TEST'
const payload = {}
const action = createAction(type, payload)
expect(action).toMatchObject({
type: type,
payload: payload
it('should create routine with metaCreator', () => {
const routine = createRoutine('test', null, () => ({ extra: true }))
stages.forEach(stage => {
const actionCreator = routine[stage.toLowerCase()]
expect(actionCreator().meta).toEqual({ extra: true })
})
})
it('should create an action object with additional properties', () => {
const type = 'TEST'
const payload = {}
const props = { test: true }
const action = createAction(type, payload, props)
expect(action).toMatchObject({
type: type,
payload: payload,
test: true
it('should create routine with different separator', () => {
const routine = createRoutine('test', null, null, { separator: '+' })
stages.forEach(stage => {
const actionCreator = routine[stage.toLowerCase()]
expect(actionCreator()).toEqual({ type: `test+${stage}` })
})
})
it('should not mutate action with additional properties', () => {
const type = 'TEST'
const props = { test: true }
const action1 = createAction('type1', null, props)
const action2 = createAction('type2', null, props)
expect(action1.type).toContain('type1')
expect(action2.type).toContain('type2')
it('should create routine with explicit stages', () => {
const stages = ['REQUEST', 'SUCCESS', 'FAILURE']
const routine = createRoutine('test', null, null, { stages: stages })
stages.forEach(stage => {
expect(routine).toHaveProperty(stage.toUpperCase())
expect(routine).toHaveProperty(stage.toLowerCase())
})
expect(routine).not.toHaveProperty('TRIGGER')
expect(routine).not.toHaveProperty('trigger')
expect(routine).not.toHaveProperty('FULFILL')
expect(routine).not.toHaveProperty('fulfill')
})
})

describe('prefixType', () => {
describe('createActionType', () => {
it('should throws if prefix is not specified', () => {
expect(prefixType).toThrow()
expect(() => createActionType()).toThrow()
})
it('should throws if prefix is a not string', () => {
expect(() => prefixType(0)).toThrow()
expect(() => prefixType(1)).toThrow()
expect(() => prefixType(true)).toThrow()
expect(() => prefixType(false)).toThrow()
expect(() => prefixType({})).toThrow()
expect(() => createActionType(0)).toThrow()
expect(() => createActionType(1)).toThrow()
expect(() => createActionType(true)).toThrow()
expect(() => createActionType(false)).toThrow()
expect(() => createActionType({})).toThrow()
})
it('should throws if stage is a not string', () => {
expect(() => createActionType('', 0)).toThrow()
expect(() => createActionType('', 1)).toThrow()
expect(() => createActionType('', true)).toThrow()
expect(() => createActionType('', false)).toThrow()
expect(() => createActionType('', {})).toThrow()
})
it('should prefix type', () => {
expect(prefixType('TEST', 'TYPE')).toBe('TEST_TYPE')
expect(createActionType('TEST', 'TYPE', '_')).toBe('TEST_TYPE')
})
it('should not modify prefix to uppercase', () => {
expect(prefixType('@@test/TEST', 'TYPE')).toBe('@@test/TEST_TYPE')
expect(createActionType('@test/TEST', 'TYPE', '_')).toBe('@test/TEST_TYPE')
})
})
21 changes: 19 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1756,7 +1756,7 @@ interpret@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90"

invariant@^2.2.0, invariant@^2.2.2:
invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
dependencies:
Expand Down Expand Up @@ -2359,11 +2359,15 @@ locate-path@^2.0.0:
p-locate "^2.0.0"
path-exists "^3.0.0"

lodash-es@^4.17.4:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7"

lodash.cond@^4.3.0:
version "4.5.2"
resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"

lodash@^4.0.0, lodash@^4.14.0, lodash@^4.2.0, lodash@^4.3.0:
lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.2.0, lodash@^4.3.0:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"

Expand Down Expand Up @@ -2841,6 +2845,19 @@ rechoir@^0.6.2:
dependencies:
resolve "^1.1.6"

reduce-reducers@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.2.tgz#fa1b4718bc5292a71ddd1e5d839c9bea9770f14b"

redux-actions@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-2.3.0.tgz#4e9967d86594b8c235bab6e08960b5c185f296d3"
dependencies:
invariant "^2.2.1"
lodash "^4.13.1"
lodash-es "^4.17.4"
reduce-reducers "^0.1.0"

regenerate@^1.2.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260"
Expand Down

0 comments on commit 7a8ce0d

Please sign in to comment.