diff --git a/src/createSlice.js b/src/createSlice.js new file mode 100644 index 0000000000..65c77dea60 --- /dev/null +++ b/src/createSlice.js @@ -0,0 +1,40 @@ +import { createReducer } from './createReducer'; + +const getType = (slice, action) => + slice ? `${slice}/${action}` : action; + +export default function createSlice({ + slice = '', + actions = {}, + initialState, +}) { + const actionKeys = Object.keys(actions); + + const reducerMap = actionKeys.reduce( + (map, action) => { + map[getType(slice, action)] = actions[action]; + return map; + }, + {}, + ); + + const reducer = createReducer(initialState, reducerMap); + + const actionMap = actionKeys.reduce( + (map, action) => { + map[action] = (payload) => ({ + type: getType(slice, action), + payload, + }); + + return map; + }, + {}, + ); + + return { + actions: actionMap, + reducer, + slice, + }; +} diff --git a/src/createSlice.test.js b/src/createSlice.test.js new file mode 100644 index 0000000000..145e7d7a8e --- /dev/null +++ b/src/createSlice.test.js @@ -0,0 +1,86 @@ +import createSlice from './createSlice' + +describe('createSlice', () => { + describe('when slice is empty', () => { + const { actions, reducer } = createSlice({ + actions: { + increment: state => state + 1, + multiply: (state, action) => state * action.payload + }, + initialState: 0 + }) + + it('should create increment action', () => { + expect(actions.hasOwnProperty('increment')).toBe(true) + }) + + it('should create multiply action', () => { + expect(actions.hasOwnProperty('multiply')).toBe(true) + }) + + it('should have the correct action for increment', () => { + expect(actions.increment()).toEqual({ + type: 'increment', + payload: undefined + }) + }) + + it('should have the correct action for multiply', () => { + expect(actions.multiply(3)).toEqual({ + type: 'multiply', + payload: 3 + }) + }) + + describe('when using reducer', () => { + it('should return the correct value from reducer with increment', () => { + expect(reducer(undefined, actions.increment())).toEqual(1) + }) + + it('should return the correct value from reducer with multiply', () => { + expect(reducer(2, actions.multiply(3))).toEqual(6) + }) + }) + }) + + describe('when passing slice', () => { + const { actions, reducer } = createSlice({ + actions: { + increment: state => state + 1 + }, + initialState: 0, + slice: 'cool' + }) + + it('should create increment action', () => { + expect(actions.hasOwnProperty('increment')).toBe(true) + }) + + it('should have the correct action for increment', () => { + expect(actions.increment()).toEqual({ + type: 'cool/increment', + payload: undefined + }) + }) + + it('should return the correct value from reducer', () => { + expect(reducer(undefined, actions.increment())).toEqual(1) + }) + }) + + describe('when mutating state object', () => { + const { actions, reducer } = createSlice({ + actions: { + setUserName: (state, action) => { + state.user = action.payload + } + }, + initialState: { user: '' }, + slice: 'user' + }) + + it('should set the username', () => { + expect(reducer({}, actions.setUserName('eric'))).toEqual({ user: 'eric' }) + }) + }) +}) diff --git a/src/index.js b/src/index.js index d3ab1832c2..95423ac86c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ export { configureStore, getDefaultMiddleware } from './configureStore' export { createReducer } from './createReducer' +export { default as createSlice } from './createSlice'; export { default as createNextState } from 'immer' export { combineReducers, compose } from 'redux'