Skip to content

Commit

Permalink
feat(combineReducers): support redux combineReducers function
Browse files Browse the repository at this point in the history
now export a combineReducers function behaving similarly as redux's one, so you can now return
effects from reducers combined together into a main one.
  • Loading branch information
matthieu-beteille committed Oct 31, 2017
1 parent e9dff24 commit 0268cc7
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 55 deletions.
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ This function will take 3 parameters when called:
- getState: useful if you need to access your state here
- dispatch: so you can dispatch new actions from there

## 3 How to use it?
## 3. How to use it?

As simple as this:

Expand All @@ -123,7 +123,8 @@ const enhancer = compose(

const store = createStore(reducer, initialState, enhancer);

// const store = createStore(reducer, initialState, reduxDataFx); if no middleware
// or createStore(reducer, enhancer); if you don't want to provide the initialState here
// or createStore(reducer, initialState, reduxDataFx); if no middleware

// then you can register as many FX as you want
store.registerFX('fetch', (params, getState, dispatch) => {
Expand All @@ -141,6 +142,21 @@ store.registerFX('dispatchLater', (params, getState, dispatch) => {

You can import ```createStore``` from 'redux'. But if you are using typescript you should import it from 'redux-data-fx' (it's the same thing except the types will be right).

### Use with ```combineReducers```

If you want this to work with the popular ```combineReducers``` function from redux, you just have to use the one from ```redux-data-fx```. You'll now be able to return effects from the reducers you're combining.

```javascript
import { reduxDataFX, combineReducers } from 'redux-data-fx'

const reducer = combinerReducers({
reducer1: reducer1,
...
});

const store = createStore(reducer, reduxDataFx);
```

## Testing

You can keep testing your reducers the same way you were doing before, except that now you can also make sure that effectful actions return the right effects descriptions. Since these descriptions are just data, it's really easy to verify that they are what you expect them to be.
Expand Down
36 changes: 22 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"main": "dist/redux-data-fx.umd.js",
"module": "dist/redux-data-fx.es5.js",
"typings": "dist/types/redux-data-fx.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"author": "Matthieu Béteille <[email protected]>",
"repository": {
"type": "git",
Expand All @@ -19,17 +21,15 @@
"scripts": {
"lint": "tslint -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
"prebuild": "rimraf dist",
"build":
"tsc && rollup -c rollup.config.ts && rimraf compiled && typedoc --out dist/docs --target es6 --theme minimal src",
"build": "tsc && rollup -c rollup.config.ts && rimraf compiled && typedoc --out dist/docs --target es6 --theme minimal src",
"start": "tsc -w & rollup -c rollup.config.ts -w",
"test": "jest",
"test:watch": "jest --watch",
"test:prod": "npm run lint && npm run test -- --coverage --no-cache",
"deploy-docs": "ts-node tools/gh-pages-publish",
"report-coverage": "cat ./coverage/lcov.info | coveralls",
"commit": "git-cz",
"semantic-release":
"semantic-release pre && npm publish && semantic-release post",
"semantic-release": "semantic-release pre && npm publish && semantic-release post",
"semantic-release-prepare": "ts-node tools/semantic-release-prepare",
"precommit": "lint-staged",
"prepush": "npm run test:prod && npm run build",
Expand All @@ -47,23 +47,29 @@
},
"validate-commit-msg": {
"types": "conventional-commit-types",
"helpMessage":
"Use \"npm run commit\" instead, we use conventional-changelog format :) (https://github.com/commitizen/cz-cli)"
"helpMessage": "Use \"npm run commit\" instead, we use conventional-changelog format :) (https://github.com/commitizen/cz-cli)"
}
},
"jest": {
"transform": {
".(ts|tsx)": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions": ["ts", "tsx", "js"],
"coveragePathIgnorePatterns": ["/node_modules/", "/test/"],
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
"/test/"
],
"coverageThreshold": {
"global": {
"branches": 60,
"functions": 60,
"lines": 60,
"statements": 60
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
},
"collectCoverage": true,
Expand Down Expand Up @@ -103,7 +109,9 @@
},
"dependencies": {
"@types/lodash.foreach": "^4.5.3",
"@types/lodash.mapvalues": "^4.6.3",
"babel-polyfill": "^6.26.0",
"lodash.foreach": "^4.5.0"
"lodash.foreach": "^4.5.0",
"lodash.mapvalues": "^4.6.0"
}
}
36 changes: 36 additions & 0 deletions src/combine-reducers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { combineReducers as reduxCombineReducers, Action } from 'redux'
import { hasFX, fx, StateWithFx } from './helpers'
import { FXReducer, BatchEffects } from './types'
import mapValues from 'lodash.mapvalues'

export interface ReducersMapObject {
[key: string]: FXReducer<any, Action>
}

function combineReducers<A extends Action>(reducers: ReducersMapObject) {
let reducer = reduxCombineReducers(reducers)

return function(state: any, action: A): StateWithFx<any> {
const newStateWithFx = reducer(state, action)
let batchEffects: BatchEffects = []

const newState = mapValues(newStateWithFx, (value: any) => {
if (hasFX(value)) {
let { state, effects } = value
if (Array.isArray(effects)) {
batchEffects = batchEffects.concat(effects)
} else {
batchEffects.push(effects)
}

return state
}

return value
})

return fx(newState, batchEffects)
}
}

export { combineReducers }
4 changes: 1 addition & 3 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export type Effects = { [key: string]: any }

export type BatchEffects = [Effects]
import { Effects, BatchEffects } from './types'

export interface StateWithFx<S> {
state: S
Expand Down
46 changes: 11 additions & 35 deletions src/redux-data-fx.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'babel-polyfill'
import forEach from 'lodash.foreach'
import { hasFX, fx, StateWithFx } from './helpers'
import { combineReducers } from './combine-reducers'
import {
StoreEnhancerStoreCreator,
Store,
Expand All @@ -10,28 +11,15 @@ import {
Dispatch,
createStore
} from 'redux'

export interface FXReducer<S, A> {
(state: S | undefined, action: A): S | StateWithFx<S>
}

export interface FXHandler<S> {
(params: FXParams, getState: () => S, dispatch: Dispatch<S>): void
}

export interface FXParams {
[key: string]: any
}

interface RegisteredFXs<S> {
[key: string]: FXHandler<S>
}

type QueuedFX = [string, FXParams]

export interface FXStore<S> extends Store<S> {
registerFX(id: string, handler: FXHandler<S>): void
}
import {
FXReducer,
FXHandler,
FXParams,
RegisteredFXs,
QueuedFX,
FXStore,
StoreCreator
} from './types'

const reduxDataFX = <S, A extends Action>(
createStore: StoreEnhancerStoreCreator<S>
Expand Down Expand Up @@ -103,18 +91,6 @@ const reduxDataFX = <S, A extends Action>(
}
}

export interface StoreCreator {
<S, A extends Action>(
reducer: FXReducer<S, A>,
enhancer?: StoreEnhancer<S>
): FXStore<S>
<S, A extends Action>(
reducer: FXReducer<S, A>,
preloadedState: S,
enhancer?: StoreEnhancer<S>
): FXStore<S>
}

const createStoreFx = createStore as StoreCreator

export { reduxDataFX, fx, createStoreFx as createStore }
export { reduxDataFX, fx, createStoreFx as createStore, combineReducers }
40 changes: 40 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Action, StoreEnhancer, Dispatch, Store } from 'redux'
import { StateWithFx } from './helpers'

export type Effects = { [key: string]: any }

export type BatchEffects = Effects[]

export interface StoreCreator {
<S, A extends Action>(
reducer: FXReducer<S, A>,
enhancer?: StoreEnhancer<S>
): FXStore<S>
<S, A extends Action>(
reducer: FXReducer<S, A>,
preloadedState: S,
enhancer?: StoreEnhancer<S>
): FXStore<S>
}

export interface FXReducer<S, A> {
(state: S | undefined, action: A): S | StateWithFx<S>
}

export interface FXHandler<S> {
(params: FXParams, getState: () => S, dispatch: Dispatch<S>): void
}

export interface FXParams {
[key: string]: any
}

export interface RegisteredFXs<S> {
[key: string]: FXHandler<S>
}

export type QueuedFX = [string, FXParams]

export interface FXStore<S> extends Store<S> {
registerFX(id: string, handler: FXHandler<S>): void
}
Loading

0 comments on commit 0268cc7

Please sign in to comment.