Skip to content

Commit 09777cc

Browse files
committed
0.3.2
- Observable selectors are now automatic - Closure creation only happens upon observation of a selector now - Can observe selectors before components are mounted; observer will be deferred until didMount.
1 parent 23d0c4a commit 09777cc

File tree

9 files changed

+130
-58
lines changed

9 files changed

+130
-58
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# 0.3
22

3+
## 0.3.2
4+
5+
### Observable selector changes
6+
7+
We've made some changes to make observable selectors more convenient. None of these are breaking changes.
8+
9+
- All selectors on all ReduxComponents are now observable by default. It is no longer necessary to use `ObservableSelectorMixin`. `ObservableSelectorMixin` will continue to exist for backwards compatibility, but is simply an empty mixin now.
10+
11+
- It is now possible to call `selector.next` even if the selector's owning component is not yet mounted. The attachment of the `Observer` to the selector will then be deferred until the component is mounted.
12+
13+
- The internal implementation of selectors has been refactored so that there is no penalty for the `Observable` implementation unless you actually attach an `Observer`.
14+
315
## 0.3.1
416

517
### Added `component.isMounted()` API

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "redux-components",
3-
"version": "0.3.1",
3+
"version": "0.3.2",
44
"description": "A component model for Redux state trees based on the React.js component model.",
55
"keywords": [
66
"redux",

src/DefaultMixin.coffee

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,28 @@
11
import { get } from './util'
2-
slice = [].slice
32

4-
# Scope a selector to a component.
5-
scopeSelector = (sel, self) -> ->
6-
fwdArgs = slice.call(arguments)
7-
fwdArgs[0] = self.state
8-
sel.apply(self, fwdArgs)
9-
10-
# Bind an action to automatically dispatch to the right store.
11-
dispatchAction = (actionCreator, self) -> ->
12-
action = actionCreator.apply(self, arguments)
13-
if action? then self.store.dispatch(action)
143

154
# DefaultMixin is mixed into all component specs automatically by createClass.
165
export default DefaultMixin = {
176
componentWillMount: ->
187
store = @store; myPath = @path
198

20-
## Scope the bits that need scoping.
9+
## Path-dependent initialization
2110
# Scope @state
2211
Object.defineProperty(@, 'state', { configurable: false, enumerable: true, get: -> get( store.getState(), myPath ) })
12+
2313
# Scope verbs
2414
if @verbs
2515
stringPath = @path.join('.')
2616
(@[verb] = "#{stringPath}:#{verb}") for verb in @verbs
27-
# Bind action creators
28-
if @actionCreators
29-
(@[acKey] = ac.bind(@)) for acKey, ac of @actionCreators
30-
# Bind actions
31-
if @actionDispatchers
32-
(@[acKey] = dispatchAction(ac, @)) for acKey, ac of @actionDispatchers
33-
# Scope selectors
34-
if @selectors
35-
(@[selKey] = scopeSelector(sel, @)) for selKey, sel of @selectors
3617

37-
# Make sure coffeescript doesn't generate an extra array here.
18+
undefined
19+
20+
componentDidMount: ->
21+
# If any observers were deferred, apply them now that we are mounted.
22+
if @__deferObservedSelectors
23+
for selector in @__deferObservedSelectors
24+
selector.__isBeingObserved(true)
25+
delete @__deferObservedSelectors
26+
3827
undefined
3928
}

src/ObservableSelectorMixin.coffee

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import { makeSelectorsObservable } from './makeSelectorObservable'
2-
31
export default ObservableSelectorMixin = {
4-
componentWillMount: ->
5-
makeSelectorsObservable(@)
2+
63
}

src/ReduxComponent.coffee

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,45 @@
11
import invariant from 'invariant'
22
import { get, nullIdentity } from './util'
33
import $$observable from 'symbol-observable'
4+
import makeSelectorObservable from './makeSelectorObservable'
5+
6+
slice = [].slice
47

58
################################
69
# Component prototype
710
export default ReduxComponent = ( -> )
811

12+
# Indirect reducer to allow components to dynamically update reducers.
913
indirectReducer = (state, action) ->
1014
@__internalReducer.call(@, state, action)
1115

16+
# Bind an action to automatically dispatch to the right store.
17+
dispatchAction = (actionCreator, self) -> ->
18+
action = actionCreator.apply(self, arguments)
19+
if action? then self.store.dispatch(action)
20+
21+
# Scope a selector to a component.
22+
scopeSelector = (sel, self) -> ->
23+
fwdArgs = slice.call(arguments)
24+
fwdArgs[0] = self.state
25+
sel.apply(self, fwdArgs)
26+
1227
ReduxComponent.prototype.__init = ->
1328
@reducer = indirectReducer.bind(@)
1429
@__internalReducer = nullIdentity
30+
31+
# Bind action creators
32+
if @actionCreators
33+
(@[key] = func.bind(@)) for key, func of @actionCreators
34+
# Bind actions
35+
if @actionDispatchers
36+
(@[key] = dispatchAction(func, @)) for key, func of @actionDispatchers
37+
# Scope selectors
38+
if @selectors
39+
for key, func of @selectors
40+
scoped = scopeSelector(func, @)
41+
@[key] = makeSelectorObservable(@, scoped)
42+
1543
undefined
1644

1745
ReduxComponent.prototype.updateReducer = ->

src/createClass.coffee

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ import ReduxComponent from './ReduxComponent'
55
dontBindThese = {
66
applyMixin: true
77
updateReducer: true
8+
isMounted: true
9+
componentWillMount: true
10+
componentDidMount: true
11+
componentWillUnmount: true
12+
getReducer: true
813
__willMount: true
914
__willUnmount: true
15+
__didMount: true
1016
__init: true
1117
}
1218

@@ -16,24 +22,24 @@ export default createClass = (spec) ->
1622
newSpec.applyMixin(newSpec, DefaultMixin)
1723
newSpec.applyMixin(newSpec, spec)
1824

19-
Constructor = ->
25+
SpecifiedReduxComponent = ->
2026
# Allow Class() instead of new Class() if desired.
21-
if not (@ instanceof Constructor) then return new Constructor()
27+
if not (@ instanceof SpecifiedReduxComponent) then return new SpecifiedReduxComponent()
2228
# Call prototype init
2329
@__init()
2430
# Magic bind all the functions on the prototype
25-
(@[k] = f.bind(@)) for k,f of Constructor.prototype when typeof(f) is 'function' and (not dontBindThese[k])
31+
(@[k] = f.bind(@)) for k,f of SpecifiedReduxComponent.prototype when typeof(f) is 'function' and (not dontBindThese[k])
2632
# Constructor must return this.
2733
@
2834

2935
# inherit from ReduxComponent
30-
Constructor.prototype = new ReduxComponent
31-
Constructor.prototype.constructor = Constructor
32-
Constructor.prototype.__spec = spec
36+
SpecifiedReduxComponent.prototype = new ReduxComponent
37+
SpecifiedReduxComponent.prototype.constructor = SpecifiedReduxComponent
38+
SpecifiedReduxComponent.prototype.__spec = spec
3339
# Apply spec to prototype, statics to constructor
3440
for own k,v of newSpec
35-
Constructor.prototype[k] = v
41+
SpecifiedReduxComponent.prototype[k] = v
3642
for own k,v of (newSpec.statics or {})
37-
Constructor[k] = v
43+
SpecifiedReduxComponent[k] = v
3844

39-
Constructor
45+
SpecifiedReduxComponent

src/makeSelectorObservable.coffee

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { removeFromList, assign } from './util'
1+
import { removeFromList } from './util'
22
import $$observable from 'symbol-observable'
33
import ReduxComponent from './ReduxComponent'
44

@@ -34,34 +34,56 @@ subjectMixin = {
3434
}
3535
}
3636

37+
# Property definition for Symbol.observable
38+
observablePropertyDefinition = { writable: true, configurable: true, value: (-> @) }
39+
40+
selectorIsBeingObserved = (isBeingObserved) ->
41+
selector = @; componentInstance = @__componentInstance
42+
if isBeingObserved
43+
lastSeenValue = undefined
44+
45+
# If component isn't mounted, defer.
46+
if not componentInstance.isMounted()
47+
componentInstance.__deferObservedSelectors = (componentInstance.__deferObservedSelectors or []).concat([selector])
48+
return
49+
50+
# Closure to detect changes in the selector.
51+
observeState = ->
52+
val = selector(componentInstance.store.getState())
53+
if val isnt lastSeenValue
54+
lastSeenValue = val; selector.next(val)
55+
56+
# Connect to the store; observe the initial state.
57+
@__unsubscriber = componentInstance.store.subscribe(observeState)
58+
observeState()
59+
else
60+
# If component isn't mounted, clear deferral.
61+
if not componentInstance.isMounted()
62+
removeFromList(componentInstance.__deferObservedSelectors, selector)
63+
return
64+
65+
# Unsubscribe from the store if previously subscribed.
66+
@__unsubscriber?(); delete @__unsubscriber
67+
undefined
68+
3769
# Make a selector on a ReduxComponentInstance into an ES7 Observable.
3870
export default makeSelectorObservable = (componentInstance, selector) ->
3971
# Make the selector a Subject.
40-
assign(selector, subjectMixin)
41-
72+
Object.assign(selector, subjectMixin)
4273
# Make the selector an ES7 Observable
43-
Object.defineProperty(selector, $$observable, { writable: true, configurable: true, value: (-> @) })
44-
45-
# Attach the selector to the Redux store when it is being observed.
46-
selector.__isBeingObserved = (isBeingObserved) ->
47-
if isBeingObserved
48-
lastSeenValue = undefined
49-
observeState = ->
50-
val = selector(componentInstance.store.getState())
51-
if val isnt lastSeenValue
52-
lastSeenValue = val; selector.next(val)
53-
54-
@__unsubscriber = componentInstance.store.subscribe(observeState)
55-
observeState()
56-
else
57-
@__unsubscriber?(); delete @__unsubscriber
58-
undefined
74+
Object.defineProperty(selector, $$observable, observablePropertyDefinition)
75+
# Store the componentInstance on the selector.
76+
selector.__componentInstance = componentInstance
77+
# Attach the observation function
78+
selector.__isBeingObserved = selectorIsBeingObserved
79+
# Return the selector
80+
selector
5981

6082
# Make all selectors on the given component instance observable.
6183
export makeSelectorsObservable = (componentInstance) ->
6284
if not (componentInstance instanceof ReduxComponent)
6385
throw new Error("makeSelectorsObservable: argument must be instanceof ReduxComponent")
64-
86+
6587
if componentInstance.selectors
6688
makeSelectorObservable(componentInstance, componentInstance[selKey]) for selKey of componentInstance.selectors
6789
componentInstance

src/util.coffee

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export get = (object, path) ->
1010
if (index is length) then object else undefined
1111

1212
export removeFromList = (list, value) ->
13-
if (i = list.indexOf(value)) > -1 then list.splice(i, 1)
13+
if list? and ((i = list.indexOf(value)) > -1) then list.splice(i, 1)
14+
undefined
1415

1516
export nullIdentity = (x) -> if x is undefined then null else x

test/04-observable-selectors.coffee

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
expectTestSequence = (tests) ->
88
i = 0
99
{
10-
next: (x) -> expect(tests[i++]?(x)).to.equal(true)
10+
next: (x) ->
11+
console.log "Observer saw", { x }
12+
expect(tests[i++]?(x)).to.equal(true)
1113
error: (x) -> throw x
1214
complete: -> expect(i).to.equal(tests.length)
1315
}
@@ -39,6 +41,7 @@ describe 'observable selectors: ', ->
3941
}
4042
selectors: {
4143
getValue: (state) ->
44+
console.log "Selector called:", { state }
4245
state.payload
4346
}
4447
}
@@ -67,3 +70,17 @@ describe 'observable selectors: ', ->
6770
rootComponentInstance.foo.setValue('goodbye world')
6871
subscription.unsubscribe()
6972
rootComponentInstance.foo.setValue('bar')
73+
74+
describe 'deferred: ', ->
75+
it 'should create new store', ->
76+
store = makeAStore({ foo: { payload: 'bar' } })
77+
78+
it 'should attach observer while unmounted', ->
79+
fooComponent = new Subcomponent
80+
rootComponentInstance = createComponent( { foo: fooComponent } )
81+
fooComponent.getValue.subscribe(
82+
expectTestSequence([ ((x) -> x is 'bar') ] )
83+
)
84+
expect(fooComponent.__deferObservedSelectors).to.be.ok
85+
mountRootComponent(store, rootComponentInstance)
86+
expect(fooComponent.__deferObservedSelectors).to.not.be.ok

0 commit comments

Comments
 (0)