Skip to content

Commit 96a5728

Browse files
author
Matthieu Beteille
committed
feat(api): update effects shape, now an array of maps
The effects used to be a single map, keys being the effects id and values the params. This API seemed a little weird especially for the redux community. Now effects are represented by maps with a structure really close to a redux action, a map with an effect key to identify the effect to run and potentially other keys to pass parameters to the handler. This makes returning a batch of effects simpler, and the api simpler, more familiar to JS users, and easier to type. BREAKING CHANGE: Effects used to be a map like { effectKey: params, effect2Key: params2 }, now should become an array of [{ effect: 'effectKey', ...params }, { effect: 'effect2Key', ...params2 }] 1
1 parent b9e3b14 commit 96a5728

File tree

7 files changed

+95
-112
lines changed

7 files changed

+95
-112
lines changed

README.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,24 @@ function reducer(state = initialState, action) {
4949
'fetch-some-data':
5050
return fx(
5151
{ ...state, isFetching: true },
52-
{
53-
fetch: {
52+
[
53+
{
54+
effect: 'fetch',
5455
url: 'http://some-api.com/data/1',
5556
method: 'GET',
5657
onSuccess: 'fetch/success',
57-
onError: 'fecth/error',
58-
},
59-
});
58+
onError: 'fetch/error'
59+
}
60+
]
61+
);
6062

6163
default:
6264
return state;
6365
}
6466
}
6567
```
6668

67-
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).
69+
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).
6870

6971
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.
7072

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

7577
- *NewState:* the new state of your app (what you usually return from your reducer)
7678

77-
- *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)
79+
- *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 }```
80+
7881

7982
*Note:* the fx function just creates an object of the following shape:
8083
```{ state: newAppState, effects: someEffectsToRun }```
@@ -98,11 +101,11 @@ store.registerFX('fetch', (params, getState, dispatch) => {
98101
});
99102
```
100103

101-
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'.
104+
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'.
102105

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

@@ -157,6 +160,10 @@ const reducer = combinerReducers({
157160
const store = createStore(reducer, reduxDataFx);
158161
```
159162

163+
### ```store.replaceReducer```
164+
165+
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.
166+
160167
### Testing
161168

162169
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.
@@ -169,6 +176,7 @@ Those are only data, so it's quite easy for you to test both of them when you te
169176

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

179+
172180
#### TODO: Default FX
173181

174182
Create some default effect handlers like:

src/combine-reducers.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,23 @@
11
import { combineReducers as reduxCombineReducers, Action } from 'redux'
22
import { hasFX, fx, StateWithFx } from './helpers'
3-
import { FXReducer, BatchEffects } from './types'
3+
import { FXReducer, Effects } from './types'
44
import mapValues from 'lodash.mapvalues'
55

66
export interface ReducersMapObject {
7-
[key: string]: FXReducer<any, Action>
7+
[key: string]: FXReducer<any>
88
}
99

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

1313
return function(state: any, action: A): StateWithFx<any> {
1414
const newStateWithFx = reducer(state, action)
15-
let batchEffects: BatchEffects = []
15+
let batchEffects: Effects = []
1616

1717
const newState = mapValues(newStateWithFx, (value: any) => {
1818
if (hasFX(value)) {
1919
let { state, effects } = value
20-
if (Array.isArray(effects)) {
21-
batchEffects = batchEffects.concat(effects)
22-
} else {
23-
batchEffects.push(effects)
24-
}
20+
batchEffects = batchEffects.concat(effects)
2521

2622
return state
2723
}

src/helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Effects, BatchEffects } from './types'
1+
import { Effects } from './types'
22

33
export interface StateWithFx<S> {
44
state: S
5-
effects: Effects | BatchEffects
5+
effects: Effects
66
}
77

88
export class StateWithFx<S> {

src/redux-data-fx.ts

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,26 @@ import {
1818
RegisteredFXs,
1919
QueuedFX,
2020
FXStore,
21-
StoreCreator
21+
StoreCreator,
22+
Effects
2223
} from './types'
2324

2425
const reduxDataFX = <S, A extends Action>(
2526
createStore: StoreEnhancerStoreCreator<S>
26-
) => (reducer: FXReducer<S, A>, initialState: S): FXStore<S> => {
27-
let q: QueuedFX[] = []
27+
) => (reducer: FXReducer<S>, initialState: S): FXStore<S> => {
28+
let q: Effects = []
2829
let fx: RegisteredFXs<S> = {}
2930

30-
const liftReducer = (reducer: FXReducer<S, A>) => (state: S, action: A) => {
31+
const liftReducer = (reducer: FXReducer<S>): Reducer<S> => (
32+
state: S,
33+
action: Action
34+
) => {
3135
const result = reducer(state, action)
3236

3337
if (hasFX(result)) {
3438
let { effects, state } = result
3539

36-
if (Array.isArray(effects)) {
37-
effects.forEach(effects => {
38-
forEach(effects, (params, id) => {
39-
q.push([id, params])
40-
})
41-
})
42-
} else {
43-
forEach(effects, (params, id) => {
44-
q.push([id, params])
45-
})
46-
}
40+
q = q.concat(effects)
4741

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

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

64-
let [id, params] = current
58+
let { effect, ...params } = current
6559

66-
if (fx[id] !== undefined) {
67-
fx[id](params, store.getState, store.dispatch)
60+
if (fx[effect] !== undefined) {
61+
// !!! performing side effects !!!
62+
fx[effect](params, store.getState, store.dispatch)
6863
} else {
6964
console.warn(
7065
'Trying to use fx: ' +
71-
id +
66+
effect +
7267
'. None has been registered. Doing nothing.'
7368
)
7469
}
@@ -77,13 +72,13 @@ const reduxDataFX = <S, A extends Action>(
7772
return res
7873
}
7974

80-
const replaceReducer = (reducer: Reducer<S>) => {
75+
const replaceEffectfulReducer = (reducer: FXReducer<S>) => {
8176
return store.replaceReducer(liftReducer(reducer))
8277
}
8378

8479
return {
8580
...store,
86-
replaceReducer,
81+
replaceEffectfulReducer,
8782
dispatch,
8883
registerFX(id: string, handler: FXHandler<S>) {
8984
fx[id] = handler

src/types.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
1-
import { Action, StoreEnhancer, Dispatch, Store } from 'redux'
1+
import { Action, StoreEnhancer, Dispatch, Store, Reducer } from 'redux'
22
import { StateWithFx } from './helpers'
33

4-
export type Effects = { [key: string]: any }
5-
6-
export type BatchEffects = Effects[]
4+
export type Effect = { effect: string; [key: string]: any }
5+
export type Effects = Effect[]
76

87
export interface StoreCreator {
98
<S, A extends Action>(
10-
reducer: FXReducer<S, A>,
9+
reducer: FXReducer<S>,
1110
enhancer?: StoreEnhancer<S>
1211
): FXStore<S>
1312
<S, A extends Action>(
14-
reducer: FXReducer<S, A>,
13+
reducer: FXReducer<S>,
1514
preloadedState: S,
1615
enhancer?: StoreEnhancer<S>
1716
): FXStore<S>
1817
}
1918

20-
export interface FXReducer<S, A> {
21-
(state: S | undefined, action: A): S | StateWithFx<S>
19+
export interface FXReducer<S> {
20+
(state: S | undefined, action: Action): S | StateWithFx<S>
2221
}
2322

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

3837
export interface FXStore<S> extends Store<S> {
3938
registerFX(id: string, handler: FXHandler<S>): void
39+
replaceEffectfulReducer(reducer: FXReducer<S>): void
4040
}

test/combine-reducers.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,16 @@ const effectfulReducer = combineReducers({
3030
reducer1: (state: number = 0, action: Action) => {
3131
switch (action.type) {
3232
case 'testFx1':
33-
return fx(state + 1, { sideFx1: action.payload })
33+
return fx(state + 1, [{ effect: 'sideFx1', ...action.payload }])
3434
case 'testFx2':
35-
return fx(state + 1, { sideFx2: action.payload })
35+
return fx(state + 1, [{ effect: 'sideFx2', ...action.payload }])
3636
case 'batchedFx':
3737
return fx(state, [
38-
{ sideFx1: {} },
39-
{ sideFx1: {} },
40-
{ sideFx2: {} },
41-
{ sideFx2: {} },
42-
{ sideFx2: {} }
38+
{ effect: 'sideFx1' },
39+
{ effect: 'sideFx1' },
40+
{ effect: 'sideFx2' },
41+
{ effect: 'sideFx2' },
42+
{ effect: 'sideFx2' }
4343
])
4444
default:
4545
return state

0 commit comments

Comments
 (0)