This repository has been archived by the owner on Sep 19, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
85edbb1
commit b663526
Showing
15 changed files
with
434 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,3 +35,5 @@ jspm_packages | |
|
||
# Optional REPL history | ||
.node_repl_history | ||
|
||
typings |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | ||
|
||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import createReducer from './reducerCreator'; | ||
export * from './reducerCreator'; | ||
export default createReducer; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'}})) | ||
|
||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import createReducer from './reducerCreator' | ||
export * from './reducerCreator' | ||
export default createReducer |
Oops, something went wrong.