Skip to content

Commit

Permalink
Merge pull request #2 from matthieu-beteille/new-api
Browse files Browse the repository at this point in the history
feat(api): update effects shape, now an array of maps
  • Loading branch information
matthieu-beteille authored Dec 5, 2017
2 parents b9e3b14 + 96a5728 commit b99ffc2
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 112 deletions.
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,24 @@ function reducer(state = initialState, action) {
'fetch-some-data':
return fx(
{ ...state, isFetching: true },
{
fetch: {
[
{
effect: 'fetch',
url: 'http://some-api.com/data/1',
method: 'GET',
onSuccess: 'fetch/success',
onError: 'fecth/error',
},
});
onError: 'fetch/error'
}
]
);

default:
return state;
}
}
```

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

If we want to run some side effects we need to return the result of the `fx` function called with your app new state and a data structure describing the side effects you want to perform.

Expand All @@ -74,7 +76,8 @@ fx(NewState, Effects)

- *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 any data required to actually perform the side effect. (for instance for an api call, you might want to provide the url, the HTTP method, and some parameters)
- *Effects:* an array containing the description of every side effect you want to run. Each side effect should be described with a map containing at least an 'effect' key, being the id of the effect you want to perform. The data required to actually perform the side effect can be passed through any other keys in the map. (for instance for an api call, you might want to provide the url, the HTTP method, and some parameters). That should remind you of the structure of a redux action: ```{ type: 'myAction1', ...params }```, except that we use the 'effect' key to identify the side effect to perform: ```{ effect: 'myEffect1', ...params }```


*Note:* the fx function just creates an object of the following shape:
```{ state: newAppState, effects: someEffectsToRun }```
Expand All @@ -98,11 +101,11 @@ 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 Effects map to describe the side effect you want to perform. In this case 'fetch'.
The first argument is the handler's id, it needs to be the same as the effect key you'll return in your reducer(s) to trigger this same effect. In this case 'fetch'.

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

Expand Down Expand Up @@ -157,6 +160,10 @@ const reducer = combinerReducers({
const store = createStore(reducer, reduxDataFx);
```

### ```store.replaceReducer```

If you want to replace some reducers (to lazyload some of them for instance), you should use the new function ```store.replaceEffectfulReducer``` from your store.

### Testing

You can keep testing your reducers the same way but when they return some effect descriptions you have now the ability to make sure these are right too.
Expand All @@ -169,6 +176,7 @@ Those are only data, so it's quite easy for you to test both of them when you te

Then you can test your effect handlers separately, to verify they run the side effects as expected given the right inputs.


#### TODO: Default FX

Create some default effect handlers like:
Expand Down
12 changes: 4 additions & 8 deletions src/combine-reducers.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
import { combineReducers as reduxCombineReducers, Action } from 'redux'
import { hasFX, fx, StateWithFx } from './helpers'
import { FXReducer, BatchEffects } from './types'
import { FXReducer, Effects } from './types'
import mapValues from 'lodash.mapvalues'

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

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 = []
let batchEffects: Effects = []

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)
}
batchEffects = batchEffects.concat(effects)

return state
}
Expand Down
4 changes: 2 additions & 2 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Effects, BatchEffects } from './types'
import { Effects } from './types'

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

export class StateWithFx<S> {
Expand Down
37 changes: 16 additions & 21 deletions src/redux-data-fx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,26 @@ import {
RegisteredFXs,
QueuedFX,
FXStore,
StoreCreator
StoreCreator,
Effects
} from './types'

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

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

if (hasFX(result)) {
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])
})
}
q = q.concat(effects)

return state
} else {
Expand All @@ -61,14 +55,15 @@ const reduxDataFX = <S, A extends Action>(

if (!current) return res // --'

let [id, params] = current
let { effect, ...params } = current

if (fx[id] !== undefined) {
fx[id](params, store.getState, store.dispatch)
if (fx[effect] !== undefined) {
// !!! performing side effects !!!
fx[effect](params, store.getState, store.dispatch)
} else {
console.warn(
'Trying to use fx: ' +
id +
effect +
'. None has been registered. Doing nothing.'
)
}
Expand All @@ -77,13 +72,13 @@ const reduxDataFX = <S, A extends Action>(
return res
}

const replaceReducer = (reducer: Reducer<S>) => {
const replaceEffectfulReducer = (reducer: FXReducer<S>) => {
return store.replaceReducer(liftReducer(reducer))
}

return {
...store,
replaceReducer,
replaceEffectfulReducer,
dispatch,
registerFX(id: string, handler: FXHandler<S>) {
fx[id] = handler
Expand Down
16 changes: 8 additions & 8 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import { Action, StoreEnhancer, Dispatch, Store } from 'redux'
import { Action, StoreEnhancer, Dispatch, Store, Reducer } from 'redux'
import { StateWithFx } from './helpers'

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

export type BatchEffects = Effects[]
export type Effect = { effect: string; [key: string]: any }
export type Effects = Effect[]

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

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

export interface FXHandler<S> {
Expand All @@ -37,4 +36,5 @@ export type QueuedFX = [string, FXParams]

export interface FXStore<S> extends Store<S> {
registerFX(id: string, handler: FXHandler<S>): void
replaceEffectfulReducer(reducer: FXReducer<S>): void
}
14 changes: 7 additions & 7 deletions test/combine-reducers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ const effectfulReducer = combineReducers({
reducer1: (state: number = 0, action: Action) => {
switch (action.type) {
case 'testFx1':
return fx(state + 1, { sideFx1: action.payload })
return fx(state + 1, [{ effect: 'sideFx1', ...action.payload }])
case 'testFx2':
return fx(state + 1, { sideFx2: action.payload })
return fx(state + 1, [{ effect: 'sideFx2', ...action.payload }])
case 'batchedFx':
return fx(state, [
{ sideFx1: {} },
{ sideFx1: {} },
{ sideFx2: {} },
{ sideFx2: {} },
{ sideFx2: {} }
{ effect: 'sideFx1' },
{ effect: 'sideFx1' },
{ effect: 'sideFx2' },
{ effect: 'sideFx2' },
{ effect: 'sideFx2' }
])
default:
return state
Expand Down
Loading

0 comments on commit b99ffc2

Please sign in to comment.