Skip to content
This repository has been archived by the owner on Sep 19, 2022. It is now read-only.

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ericwooley committed Sep 17, 2016
1 parent 85edbb1 commit b663526
Show file tree
Hide file tree
Showing 15 changed files with 434 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ jspm_packages

# Optional REPL history
.node_repl_history

typings
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,51 @@
# easy-reducer
Easier reducers with less boilerplate.

# Using easy reducers

```js
import easyReducerCreator from '../reducerCreator'
import { createStore, applyMiddleware, combineReducers } from 'redux'
import thunk from 'redux-thunk'

const defaultState {
test: 'data'
}
const syncActions = {
// previous state is always the last argument.
testDataPlusNum (num, state) {
return {...state, test: 'data' + num}
},
testDataPlusTwoNum (num1, num2, state) {
return {...state, test: 'data' + num1 + '' + num2}
}
}

// async actions are handled differently, and are meant to be used with redux-thunk
const asyncActions = {
// syncActions, dispatch, getState (from thunk), are always the last arguments.
asyncStateModifyer (num, syncActions, dispatch, getState) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(dispatch(syncActions.testDataPlusNum(num)))
}, 20)
})
},
}

// Test reducer now has all the action creators you would use for your methods, with the types TR1/methodName
export const testReducer1 = reducerCreator(defaultState, syncActions, asyncActions)('TR1')
// Reducers are reusable with different ID's
export const testReducer2 = reducerCreator(defaultState, syncActions, asyncActions)('TR2')

export const store = createStore(combineReducers({
// a reducer property is attached, which serves as the actual reducer.
testReducer1: testReducer1.reducer,
testReducer2: testReducer2.reducer
}), undefined, applyMiddleware(thunk))

store.dispatch(testReducer1.testDataPlusNum(1))
store.dispatch(testReducer1.asyncStateModifyer(2))
.then(() => store.dispatch(testReducer2.testDataPlusTwoNum(3, 4)))

```
3 changes: 3 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import createReducer from './reducerCreator';
export * from './reducerCreator';
export default createReducer;
9 changes: 9 additions & 0 deletions lib/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/index.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions lib/reducerCreator.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface IReducer {
reducer: Function;
}
export default function makeReducer<ISubState, T, V>(defaultState: ISubState, Actions?: T, AsyncActions?: V): (ID: string) => V & T & IReducer;
60 changes: 60 additions & 0 deletions lib/reducerCreator.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/reducerCreator.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "easy-reducer",
"version": "0.1.0",
"description": "Easy reducers with less boilerplate",
"main": "lib/",
"scripts": {
"prepublish": "rm -rf lib && typings install && npm run test && tsc",
"test": "jest"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ericwooley/easy-reducer.git"
},
"keywords": [
"redux",
"react",
"reducer",
"elm",
"frp",
"thunk"
],
"author": "Eric Wooley <[email protected]>",
"license": "MIT",
"bugs": {
"url": "https://github.com/ericwooley/easy-reducer/issues"
},
"homepage": "https://github.com/ericwooley/easy-reducer#readme",
"devDependencies": {
"jest-cli": "^15.1.1",
"redux": "^3.6.0",
"redux-thunk": "^2.1.0",
"typescript": "^1.8.10",
"typings": "^1.3.3"
},
"jest": {
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"scriptPreprocessor": "<rootDir>/preprocessor.js",
"testRegex": "src/__tests__/.*\\.(ts|tsx|js)$"
},
"dependencies": {
"lodash": "^4.15.0",
"lodash.merge": "^4.6.0",
"object-assign": "^4.1.0"
}
}
19 changes: 19 additions & 0 deletions preprocessor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2004-present Facebook. All Rights Reserved.

const tsc = require('typescript');

module.exports = {
process(src, path) {
if (path.endsWith('.ts')) {
return tsc.transpile(
src,
{
module: tsc.ModuleKind.CommonJS
},
path,
[]
);
}
return src;
},
};
152 changes: 152 additions & 0 deletions src/__tests__/reducerCreator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@

import reducerCreator from '../reducerCreator'
import { createStore, applyMiddleware, combineReducers } from 'redux'
import thunk from 'redux-thunk'
import {merge} from 'lodash'

describe('reducerCreator', () => {
it('should exist', () => {
expect(reducerCreator).not.toBe(undefined)
})
it('should throw an error if there is no id', () => {
expect(() => (reducerCreator as any)()()).toThrowError('Reducers must have an ID')
})
it('should throw an error if there is no default state', () => {
expect(() => (reducerCreator as any)()('ID')).toThrowError('Reducers must have a default state')
})
describe('sync actions', () => {
let testReducer
let defaultState = {
test: 'data'
}
let syncActions
beforeEach(() => {
syncActions = {
testDataPlus1: jest.fn(),
testDataPlusNumber: jest.fn(),
testDataPlusManyNumbers: jest.fn()
}
testReducer = reducerCreator(defaultState, syncActions)('ID')
})
it('should auto create actions based on method names', () => {
expect(testReducer.testDataPlus1).toBeTruthy()
expect(testReducer.testDataPlus1()).toEqual({type: 'ID/testDataPlus1', payload: []})
expect(testReducer.testDataPlusNumber()).toEqual({type: 'ID/testDataPlusNumber', payload: []})
})
it('should keep arguments as an array', () => {
expect(testReducer.testDataPlusManyNumbers(1, 2, 3, 4))
.toEqual({type: 'ID/testDataPlusManyNumbers', payload: [1, 2, 3, 4]})
})
})
describe('reducing', () => {
let testReducer
let defaultState = {
test: 'data'
}
let syncActions
beforeEach(() => {
syncActions = {
testDataPlus1 (state) {
return merge({}, state, {test: 'data1'})
},
testSpy: jest.fn(),
testDataPlusANumber (num, state) {
return merge({}, state, {test: state.test + num})
}
}
testReducer = reducerCreator(defaultState, syncActions)('ID')
})
it('should have a reducer method', () => {
expect(testReducer.reducer).toBeTruthy()
})
it('should call actions', () => {
// the original funciton is replaced by an action
testReducer.reducer(defaultState, testReducer.testSpy())
expect(syncActions.testSpy).toBeCalled()
})

it('should pass state as the first argument when there are no arguments', () => {
testReducer.reducer(defaultState, testReducer.testSpy())
expect(syncActions.testSpy).toBeCalledWith(defaultState)
})

it('should pass in arugments before state', () => {
testReducer.reducer(defaultState, testReducer.testSpy(1))
expect(syncActions.testSpy).toBeCalledWith(1, defaultState)
testReducer.reducer(defaultState, testReducer.testSpy(1, 2))
expect(syncActions.testSpy).toBeCalledWith(1, 2, defaultState)
})
it('should not modify the state for actions it does not own', () => {
let newState = testReducer.reducer(defaultState, {type: 'some other action'})
expect(newState).toBe(defaultState)
})
it('should modify state', () => {
let newState = testReducer.reducer(defaultState, testReducer.testDataPlus1())
expect(newState).toEqual({test: 'data1'})
expect(newState).not.toEqual(defaultState)
let newState2 = testReducer.reducer(newState, testReducer.testDataPlusANumber(2))
expect(newState2).toEqual({test: 'data12'})
})
})
describe('async actions', () => {
let testStore
let testReducer
let defaultState = {
test: 'data'
}
let syncActions, asyncActions, spy

beforeEach(() => {
spy = jest.fn()
asyncActions = {
asyncTestSpy: jest.fn(),
asyncTestSpyCaller (num, syncActions, dispatch, getState) {
dispatch(syncActions.testSpy(num))
},
asyncStateModifyer (num, syncActions, dispatch, getState) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(dispatch(syncActions.testDataPlusNum(num)))
}, 20)
})
},
asyncTimeoutTest (syncActions, dispatch, getState) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(dispatch(syncActions.testSpy()))
}, 20)
})
},
asyncTestDataPlus1 (syncActions, dispatch, getState) {
dispatch(syncActions.testSpy())
}
}
syncActions = {
testDataPlusNum (num, state) {
return merge({}, state, {test: 'data' + num})
},
testSpy: (state) => spy() || merge({}, state),
}
testReducer = reducerCreator(defaultState, syncActions, asyncActions)('ID')
testStore = createStore(combineReducers({testReducer: testReducer.reducer}), undefined, applyMiddleware(thunk))
})
it('should call the async spy', () => {
testStore.dispatch(testReducer.asyncTestSpy())
expect(asyncActions.asyncTestSpy).toBeCalled()
})
it('should call sync test spy', () => {
testStore.dispatch(testReducer.asyncTestSpyCaller(1))
expect(spy).toBeCalled()
})
it('it should work asynchronously', function () {
return testStore.dispatch(testReducer.asyncTimeoutTest())
.then(() => expect(spy).toBeCalled())

})
it('it should modify state asynchronously', function () {
return testStore.dispatch(testReducer.asyncStateModifyer(1))
.then(() => expect(testStore.getState()).toEqual({testReducer: {test: 'data1'}}))

})
})
})
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import createReducer from './reducerCreator'
export * from './reducerCreator'
export default createReducer
Loading

0 comments on commit b663526

Please sign in to comment.