Skip to content

Commit d38c492

Browse files
Merge pull request cerebral#31 from cerebral/reactions
Reactions
2 parents d83e92f + 8fa0808 commit d38c492

File tree

9 files changed

+456
-64
lines changed

9 files changed

+456
-64
lines changed

packages/node_modules/overmind/src/index.ts

Lines changed: 174 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ import { EventEmitter } from 'betsy'
33
import ProxyStateTree from 'proxy-state-tree'
44
import Devtools, { Message, safeValue } from './Devtools'
55
import Action, { IValueAction, INoValueAction } from './Action'
6+
import Reaction, { ReactionConfig } from './reaction'
67
export { default as namespaces, Namespace } from './namespaces'
78
export { default as derived } from './derived'
89
export { default as computed } from './computed'
910

10-
type Configuration<State, Providers, Actions> = {
11+
type Configuration<State, Providers, Actions, Reactions> = {
1112
state?: State
1213
providers?: Providers
1314
actions?: Actions
15+
reactions?: Reactions
1416
}
1517

1618
type Options = {
@@ -33,12 +35,32 @@ export interface Events {
3335
cacheKeysCount: number
3436
cacheKeyIndex: number
3537
}
38+
'reaction:add': {
39+
path: string
40+
statePath: string
41+
updateCount: number
42+
}
43+
'reaction:update': {
44+
path: string
45+
statePath: string
46+
updateCount: number
47+
}
48+
'reaction:remove': {
49+
path: string
50+
statePath: string
51+
updateCount: number
52+
}
3653
}
3754

3855
export type ActionsCallback<Providers, State> = (
3956
action: IAction<State, Providers & { state: State }>
4057
) => any
4158

59+
export type ReactionsCallback<State, Providers> = (
60+
reaction: ReactionConfig<State, Providers>,
61+
action: IAction<State, Providers & { state: State }>
62+
) => any
63+
4264
export type TContext<State, Providers = {}> = Providers & {
4365
state: State
4466
}
@@ -52,31 +74,49 @@ export interface IAction<State, Context> {
5274
export default class App<
5375
State extends object,
5476
Providers extends object,
77+
Reactions extends
78+
| ReactionsCallback<State, Providers>
79+
| {
80+
[namespace: string]: ReactionsCallback<State, any>
81+
},
5582
Actions extends
83+
| ActionsCallback<Providers, State>
5684
| {
5785
[namespace: string]: ActionsCallback<{}, {}>
5886
}
59-
| ActionsCallback<Providers, State>
6087
> {
6188
private proxyStateTree: ProxyStateTree
89+
private eventHub: EventEmitter<Events>
6290
devtools: Devtools
63-
actions: Actions extends {
64-
[namespace: string]: ActionsCallback<{}, {}>
65-
}
66-
? { [Namespace in keyof Actions]: ReturnType<Actions[Namespace]> }
67-
: Actions extends ActionsCallback<Providers, State>
68-
? ReturnType<Actions>
91+
actions: Actions extends ActionsCallback<Providers, State>
92+
? ReturnType<Actions>
93+
: Actions extends {
94+
[namespace: string]: ActionsCallback<{}, {}>
95+
}
96+
? { [Namespace in keyof Actions]: ReturnType<Actions[Namespace]> }
6997
: any
7098
state: State
7199
constructor(
72-
configuration: Configuration<State, Providers, Actions>,
100+
configuration: Configuration<State, Providers, Actions, Reactions>,
73101
options: Options = {}
74102
) {
103+
/*
104+
Set up an eventHub to trigger information from derived, computed and reactions
105+
*/
75106
const eventHub = new EventEmitter<Events>()
107+
108+
/*
109+
Create the proxy state tree instance with the state and a wrapper to expose
110+
the eventHub
111+
*/
76112
const proxyStateTree = new ProxyStateTree(configuration.state || {}, {
77113
dynamicWrapper: (proxyStateTree, path, func) =>
78114
func(eventHub, proxyStateTree, path),
79115
})
116+
117+
/*
118+
The action chain with the context configuration
119+
*/
80120
const actionChain = new ActionChain(
81121
Object.assign(
82122
{
@@ -88,12 +128,30 @@ export default class App<
88128
providerExceptions: ['state'],
89129
}
90130
)
131+
132+
/*
133+
The action factory function
134+
*/
91135
const action = function<InitialValue>(): [InitialValue] extends [void]
92136
? INoValueAction<State, Providers & { state: State }, InitialValue>
93137
: IValueAction<State, Providers & { state: State }, InitialValue> {
94138
return new Action(proxyStateTree, actionChain) as any
95139
}
96140

141+
if (options.devtools && typeof window !== 'undefined') {
142+
this.initializeDevtools(
143+
options.devtools,
144+
actionChain,
145+
eventHub,
146+
proxyStateTree
147+
)
148+
}
149+
150+
this.initializeReactions(configuration, eventHub, proxyStateTree, action)
151+
152+
/*
153+
Identify when the state tree should flush out changes
154+
*/
97155
actionChain.on('operator:async', () => {
98156
if (this.devtools) {
99157
this.devtools.send({
@@ -113,29 +171,16 @@ export default class App<
113171
proxyStateTree.flush()
114172
})
115173

174+
/*
175+
Expose the created actions
176+
*/
177+
this.actions = this.getActions(configuration, action)
178+
116179
this.state = proxyStateTree.get()
117-
this.actions =
118-
typeof configuration.actions === 'function'
119-
? (configuration.actions as ActionsCallback<Providers, State>)(
120-
action as IAction<State, Providers & { state: State }>
121-
)
122-
: (Object.keys(configuration.actions || {}).reduce(
123-
(aggr, namespace) =>
124-
Object.assign(aggr, {
125-
[namespace]: configuration.actions[namespace](action as IAction<
126-
State,
127-
Providers & { state: State }
128-
>),
129-
}),
130-
{}
131-
) as any)
132180
this.proxyStateTree = proxyStateTree
133-
134-
if (options.devtools && typeof window !== 'undefined') {
135-
this.initializeDevtools(options.devtools, actionChain, eventHub)
136-
}
181+
this.eventHub = eventHub
137182
}
138-
private initializeDevtools(host, actionChain, eventHub) {
183+
private initializeDevtools(host, actionChain, eventHub, proxyStateTree) {
139184
const devtools = new Devtools()
140185
devtools.connect(
141186
host,
@@ -146,7 +191,7 @@ export default class App<
146191
devtools.send({
147192
type: 'init',
148193
data: {
149-
state: this.proxyStateTree.get(),
194+
state: proxyStateTree.get(),
150195
},
151196
})
152197
actionChain.on('action:start', (data) =>
@@ -200,8 +245,87 @@ export default class App<
200245
data,
201246
})
202247
)
248+
eventHub.on('reaction:add', (data) =>
249+
devtools.send({
250+
type: 'reaction:add',
251+
data,
252+
})
253+
)
254+
eventHub.on('reaction:update', (data) =>
255+
devtools.send({
256+
type: 'reaction:update',
257+
data,
258+
})
259+
)
260+
eventHub.on('reaction:remove', (data) =>
261+
devtools.send({
262+
type: 'reaction:remove',
263+
data,
264+
})
265+
)
203266
this.devtools = devtools
204267
}
268+
private initializeReactions(configuration, eventHub, proxyStateTree, action) {
269+
if (typeof configuration.reactions === 'function') {
270+
const reactions = (configuration.reactions as any)(
271+
(stateCb, action) => [stateCb, action],
272+
action
273+
)
274+
Object.keys(reactions).forEach((key) => {
275+
const reaction = new Reaction(eventHub, proxyStateTree, key)
276+
reaction.create(reactions[key][0], reactions[key][1])
277+
})
278+
} else {
279+
const reactions = Object.keys(configuration.reactions || {}).reduce(
280+
(aggr, namespace) =>
281+
Object.assign(
282+
aggr,
283+
configuration.reactions[namespace]
284+
? {
285+
[namespace]: (configuration.reactions[namespace] as any)(
286+
(stateCb, action) => [stateCb, action],
287+
action
288+
),
289+
}
290+
: {}
291+
),
292+
{}
293+
)
294+
Object.keys(reactions).forEach((namespace) => {
295+
Object.keys(reactions[namespace]).forEach((key) => {
296+
const reaction = new Reaction(
297+
eventHub,
298+
proxyStateTree,
299+
namespace + '.' + key
300+
)
301+
reaction.create(
302+
reactions[namespace][key][0],
303+
reactions[namespace][key][1]
304+
)
305+
})
306+
})
307+
}
308+
}
309+
private getActions(configuration, action) {
310+
return typeof configuration.actions === 'function'
311+
? (configuration.actions as ActionsCallback<Providers, State>)(
312+
action as IAction<State, Providers & { state: State }>
313+
)
314+
: (Object.keys(configuration.actions || {}).reduce(
315+
(aggr, namespace) =>
316+
Object.assign(
317+
aggr,
318+
configuration.actions[namespace]
319+
? {
320+
[namespace]: configuration.actions[namespace](
321+
action as IAction<State, Providers & { state: State }>
322+
),
323+
}
324+
: {}
325+
),
326+
{}
327+
) as any)
328+
}
205329
trackState() {
206330
return this.proxyStateTree.startPathsTracking()
207331
}
@@ -211,4 +335,24 @@ export default class App<
211335
addMutationListener(paths, cb) {
212336
return this.proxyStateTree.addMutationListener(paths, cb)
213337
}
338+
createReactionFactory(prefix: string) {
339+
const reactions = []
340+
const instance = this
341+
return {
342+
add(name: string, stateCb: (state: State) => any, cb: Function) {
343+
const reaction = new Reaction(
344+
instance.eventHub,
345+
instance.proxyStateTree,
346+
prefix + '.' + name
347+
)
348+
reaction.create(stateCb, cb)
349+
350+
reactions.push(reaction)
351+
},
352+
dispose() {
353+
reactions.forEach((reaction) => reaction.destroy())
354+
reactions.length = 0
355+
},
356+
}
357+
}
214358
}

packages/node_modules/overmind/src/namespaces.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ type Module = {
44
state?: any
55
providers?: any
66
actions?: any
7+
reactions?: any
78
}
89

910
type ModuleFunction = (namespace: string) => Module
@@ -36,6 +37,13 @@ export default function namespaces<Namespaces extends TNamespaces>(
3637
? Namespaces[Namespace]['actions']
3738
: any
3839
}
40+
reactions: {
41+
[Namespace in keyof Namespaces]: Namespaces[Namespace] extends ModuleFunction
42+
? ReturnType<Namespaces[Namespace]>['reactions']
43+
: Namespaces[Namespace] extends Module
44+
? Namespaces[Namespace]['reactions']
45+
: any
46+
}
3947
} {
4048
return Object.keys(namespaces).reduce(
4149
(aggr, key) => {
@@ -55,14 +63,19 @@ export default function namespaces<Namespaces extends TNamespaces>(
5563
},
5664
actions: {
5765
...aggr.actions,
58-
[key]: namespace.actions || {},
66+
[key]: namespace.actions,
67+
},
68+
reactions: {
69+
...aggr.reactions,
70+
[key]: namespace.reactions,
5971
},
6072
})
6173
},
6274
{
6375
state: {},
6476
providers: {},
6577
actions: {},
78+
reactions: {},
6679
}
6780
)
6881
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import App from './'
2+
3+
describe('Reaction', () => {
4+
test('should instantiate app with reactions', () => {
5+
let hasRunReaction = false
6+
const app = new App({
7+
state: {
8+
foo: 'bar',
9+
},
10+
actions: (action) => ({
11+
foo: action().mutation((_, state) => (state.foo = 'bar2')),
12+
}),
13+
reactions: (reaction, action) => ({
14+
foo: reaction(
15+
(state) => state.foo,
16+
action().do(() => {
17+
hasRunReaction = true
18+
})
19+
),
20+
}),
21+
})
22+
app.actions.foo()
23+
expect(hasRunReaction).toBe(true)
24+
})
25+
test('should react to nested changes', () => {
26+
let hasRunReaction = false
27+
const app = new App({
28+
state: {
29+
foo: [
30+
{
31+
completed: false,
32+
},
33+
],
34+
},
35+
actions: (action) => ({
36+
foo: action().mutation((_, state) => (state.foo[0].completed = true)),
37+
}),
38+
reactions: (reaction, action) => ({
39+
foo: reaction(
40+
(state) => state.foo,
41+
action().do(() => {
42+
hasRunReaction = true
43+
})
44+
),
45+
}),
46+
})
47+
app.actions.foo()
48+
expect(hasRunReaction).toBe(true)
49+
})
50+
})

0 commit comments

Comments
 (0)