Skip to content

Commit

Permalink
feat(fx): create fx function
Browse files Browse the repository at this point in the history
you now need to use the function fx(newState, Effects) from the package to retu

BREAKING CHANGE: no more _fx field, now use fx function
  • Loading branch information
matthieu-beteille committed Oct 25, 2017
1 parent 949086b commit e9dff24
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 119 deletions.
54 changes: 30 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

Declarative Side Effects for Redux.

There are many options out there to handle side effects with Redux (redux-coco, redux-blabla, redux-gnagna, redux-hiprx...). I didn't quite find what I wanted so I decided I'd made this small store enhancer..
There are many options out there to handle side effects with Redux (redux-saga, redux-loop...). I didn't quite find what I wanted so I decided I'd made this small store enhancer.

The idea is very simple: in addition of the new state, your reducers can now return a data structure describing the side effects you want to run.

This is very similar to `redux-loop`, mostly inspired by the elm architecture. But I'd say this implementation's idea comes from re-frame in cljs and its effectful handlers.
The idea is very simple: in addition of the new state, your reducers can now also return a data structure describing some side effects you want to run. (Your reducers remain pure functions.)
This is very similar to `redux-loop`, mostly inspired by the elm architecture. But I'd say this very implementation's idea comes from re-frame in cljs and its effectful handlers.

(re-frame is an awesome project, you should definitely check it out https://github.com/Day8/re-frame/)

Expand All @@ -18,15 +18,17 @@ Usual reducer signature:

With redux-data-fx, it becomes:

```(Action, State) -> State | { newState: State, _fx: Effects }```
```(Action, State) -> State | { state: State, effects: Effects }```

Your reducer can either return just a new state as usual (same as first signature), or a combination of a new state and another data structure: the description of some side effects that you want to run.
Your reducer can either return only a new state as usual (same as first signature), or a combination of a new state and another data structure: the description of some side effects that you want to run.

## 1. Declaratively describe side effects in your reducers.

One of your reducer could now look like this:

```javascript
import { fx } from 'redux-data-fx';

function reducer(state = initialState, action) {
switch(action.type) {
'noop':
Expand All @@ -47,40 +49,41 @@ function reducer(state = initialState, action) {
};

'fetch-some-data':
return {
newState: { ...state, isFetching: true },
_fx: {
return fx(
{ ...state, isFetching: true },
{
fetch: {
url: 'http://some-api.com/data/1',
method: 'GET',
onSuccess: 'fetch/success',
onError: 'fecth/error',
},
}};
});

default:
return state;
}
}
```

The 'fetch-some-data' action is what we can call an effectful action, it updates the state and returns a description of the side effects to run as a result.
The 'fetch-some-data' action is what we call an effectful action, it updates the state and returns a description of some side effects to run as a result (here an http call).

As you probably have understood already, if we want to run some side effects we need to return the result of the `fx` function (```from 'redux-data-fx'```) called with your app new state and a data structure describing the side effects you want to perform.


As you probably have understood already, if we want to run some side effects we return a map with two fields:
```javascript
{
newState: 'the new state of your app (what you usually returns from your reducer)',
_fx: 'a map containing the description of all the side effects you want to perform'
}
fx(NewState, Effects)
```

*IMPORTANT NOTE*: if your real app state is a map containing a `_fx` field and `newState` field, then that won't work. This is a trade off I am willing to accept for now, since I find this solution really convenient, but we can definitely discuss new approaches (similar to redux-loop for instance).
- *NewState:* the new state of your app (what you usually return from your reducer)

- *Effects:* a map containing the description of all the side effects you want to run. The keys of this map are the id/names of the side effects. The values are any data structures containing the data required to actually perform the side effect. (for instance for an api call, you might want to put the url, the HTTP method, and some parameters in there)

## 2. How to run those described side effects ?
## 2. Run side effects

In order to actually run these side effects you'll need to register some effects handlers.

This is where the effectful nasty code will run (at the border of the system).
This is where the effectful code will be run (at the border of the system).

For instance to run our fetch side effects we could register the following handler:

Expand All @@ -95,13 +98,13 @@ store.registerFX('fetch', (params, getState, dispatch) => {
});
```

The first argument is the handler's id, it needs to be the same as the key used in the `_fx` map to describe the side effect you want to perfor. In this case 'fetch'.
The first argument is the handler's id, it needs to be the same as the key used in the Effects map to describe the side effect you want to perform. In this case 'fetch'.

The second argument is the effect handler, the effectful function that will perform this side effect.
It takes 3 parameters:
- the description of the effect to run (from the _fx object you returned in the reducer)
- getState useful if you need to access your state here
- dispatch so you can dispatch new actions from there
This function will take 3 parameters when called:
- the description of the effect to run (from the Effects map you returned in the reducer)
- getState: useful if you need to access your state here
- dispatch: so you can dispatch new actions from there

## 3 How to use it?

Expand Down Expand Up @@ -136,6 +139,8 @@ 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).

## 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 All @@ -144,4 +149,5 @@ Then you can test your effect handlers independantly, to make sure they run the

## TODO: Default FX

Make it work with combineReducers.
Create some default effect handlers like: fetch, localStorage, sessionStorage, dispatchLater, dispatch...
32 changes: 13 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
"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 @@ -21,15 +19,17 @@
"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,29 +47,23 @@
},
"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": 80,
"functions": 80,
"lines": 80,
"statements": 80
"branches": 60,
"functions": 60,
"lines": 60,
"statements": 60
}
},
"collectCoverage": true,
Expand Down
Empty file removed src/fx.js
Empty file.
26 changes: 20 additions & 6 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
export interface StateWithFx {
[key: string]: any
newState: any
_fx: { [key: string]: any }
export type Effects = { [key: string]: any }

export type BatchEffects = [Effects]

export interface StateWithFx<S> {
state: S
effects: Effects | BatchEffects
}

export class StateWithFx<S> {
constructor(state: S, effects: Effects) {
this.state = state
this.effects = effects
}
}

export function hasFX<S>(s: S | StateWithFx<S>): s is StateWithFx<S> {
return s instanceof StateWithFx
}

export function hasFX(s: any): s is StateWithFx {
return s && typeof s._fx === 'object' && '_fx' in s && 'newState' in s
export function fx<S>(state: S, effects: Effects): StateWithFx<S> {
return new StateWithFx(state, effects)
}
57 changes: 42 additions & 15 deletions src/redux-data-fx.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import 'babel-polyfill'
import forEach from 'lodash.foreach'
import { hasFX } from './helpers'
import { hasFX, fx, StateWithFx } from './helpers'
import {
StoreCreator,
StoreEnhancerStoreCreator,
Store,
Reducer,
StoreEnhancer,
Action,
Dispatch
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
}
Expand All @@ -25,26 +29,35 @@ interface RegisteredFXs<S> {

type QueuedFX = [string, FXParams]

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

const reduxDataFX = <S>(createStore: StoreEnhancerStoreCreator<S>) => (
reducer: Reducer<S>,
initialState: S
): EnhancedStore<S> => {
const reduxDataFX = <S, A extends Action>(
createStore: StoreEnhancerStoreCreator<S>
) => (reducer: FXReducer<S, A>, initialState: S): FXStore<S> => {
let q: QueuedFX[] = []
let fx: RegisteredFXs<S> = {}

const liftReducer = (reducer: Reducer<S>) => (state: S, action: Action) => {
const liftReducer = (reducer: FXReducer<S, A>) => (state: S, action: A) => {
const result = reducer(state, action)

if (hasFX(result)) {
let { _fx, newState } = result
forEach(_fx, (params, id) => {
q.push([id, params])
})
return newState
let { effects, state } = result

if (Array.isArray(effects)) {
effects.forEach(effects => {
forEach(effects, (params, id) => {
q.push([id, params])
})
})
} else {
forEach(effects, (params, id) => {
q.push([id, params])
})
}

return state
} else {
return result
}
Expand Down Expand Up @@ -90,4 +103,18 @@ const reduxDataFX = <S>(createStore: StoreEnhancerStoreCreator<S>) => (
}
}

export { reduxDataFX }
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 }
Loading

0 comments on commit e9dff24

Please sign in to comment.