@@ -6,6 +6,30 @@ import Subscription from '../utils/Subscription'
6
6
import storeShape from '../utils/storeShape'
7
7
8
8
let hotReloadingVersion = 0
9
+ const dummyState = { }
10
+ function noop ( ) { }
11
+
12
+ function makeSelectorStateful ( sourceSelector , store ) {
13
+ // wrap the selector in an object that tracks its results between runs.
14
+ const selector = {
15
+ run : function runComponentSelector ( props ) {
16
+ try {
17
+ const nextProps = sourceSelector ( store . getState ( ) , props )
18
+ if ( nextProps !== selector . props || selector . error ) {
19
+ selector . shouldComponentUpdate = true
20
+ selector . props = nextProps
21
+ selector . error = null
22
+ }
23
+ } catch ( error ) {
24
+ selector . shouldComponentUpdate = true
25
+ selector . error = error
26
+ }
27
+ }
28
+ }
29
+
30
+ return selector
31
+ }
32
+
9
33
export default function connectAdvanced (
10
34
/*
11
35
selectorFactory is a func that is responsible for returning the selector function used to
@@ -96,28 +120,27 @@ export default function connectAdvanced(
96
120
this . version = version
97
121
this . state = { }
98
122
this . renderCount = 0
99
- this . store = this . props [ storeKey ] || this . context [ storeKey ]
100
- this . parentSub = props [ subscriptionKey ] || context [ subscriptionKey ]
101
-
123
+ this . store = props [ storeKey ] || context [ storeKey ]
124
+ this . propsMode = Boolean ( props [ storeKey ] )
102
125
this . setWrappedInstance = this . setWrappedInstance . bind ( this )
103
126
104
127
invariant ( this . store ,
105
- `Could not find "${ storeKey } " in either the context or ` +
106
- `props of "${ displayName } ". ` +
107
- `Either wrap the root component in a <Provider>, ` +
128
+ `Could not find "${ storeKey } " in either the context or props of ` +
129
+ `"${ displayName } ". Either wrap the root component in a <Provider>, ` +
108
130
`or explicitly pass "${ storeKey } " as a prop to "${ displayName } ".`
109
131
)
110
132
111
- // make sure `getState` is properly bound in order to avoid breaking
112
- // custom store implementations that rely on the store's context
113
- this . getState = this . store . getState . bind ( this . store ) ;
114
-
115
133
this . initSelector ( )
116
134
this . initSubscription ( )
117
135
}
118
136
119
137
getChildContext ( ) {
120
- return { [ subscriptionKey ] : this . subscription || this . parentSub }
138
+ // If this component received store from props, its subscription should be transparent
139
+ // to any descendants receiving store+subscription from context; it passes along
140
+ // subscription passed to it. Otherwise, it shadows the parent subscription, which allows
141
+ // Connect to control ordering of notifications to flow top-down.
142
+ const subscription = this . propsMode ? null : this . subscription
143
+ return { [ subscriptionKey ] : subscription || this . context [ subscriptionKey ] }
121
144
}
122
145
123
146
componentDidMount ( ) {
@@ -144,12 +167,11 @@ export default function connectAdvanced(
144
167
145
168
componentWillUnmount ( ) {
146
169
if ( this . subscription ) this . subscription . tryUnsubscribe ( )
147
- // these are just to guard against extra memory leakage if a parent element doesn't
148
- // dereference this instance properly, such as an async callback that never finishes
149
170
this . subscription = null
171
+ this . notifyNestedSubs = noop
150
172
this . store = null
151
- this . parentSub = null
152
- this . selector . run = ( ) => { }
173
+ this . selector . run = noop
174
+ this . selector . shouldComponentUpdate = false
153
175
}
154
176
155
177
getWrappedInstance ( ) {
@@ -165,65 +187,63 @@ export default function connectAdvanced(
165
187
}
166
188
167
189
initSelector ( ) {
168
- const { dispatch } = this . store
169
- const { getState } = this ;
170
- const sourceSelector = selectorFactory ( dispatch , selectorFactoryOptions )
171
-
172
- // wrap the selector in an object that tracks its results between runs
173
- const selector = this . selector = {
174
- shouldComponentUpdate : true ,
175
- props : sourceSelector ( getState ( ) , this . props ) ,
176
- run : function runComponentSelector ( props ) {
177
- try {
178
- const nextProps = sourceSelector ( getState ( ) , props )
179
- if ( selector . error || nextProps !== selector . props ) {
180
- selector . shouldComponentUpdate = true
181
- selector . props = nextProps
182
- selector . error = null
183
- }
184
- } catch ( error ) {
185
- selector . shouldComponentUpdate = true
186
- selector . error = error
187
- }
188
- }
189
- }
190
+ const sourceSelector = selectorFactory ( this . store . dispatch , selectorFactoryOptions )
191
+ this . selector = makeSelectorStateful ( sourceSelector , this . store )
192
+ this . selector . run ( this . props )
190
193
}
191
194
192
195
initSubscription ( ) {
193
- if ( shouldHandleStateChanges ) {
194
- const subscription = this . subscription = new Subscription ( this . store , this . parentSub )
195
- const dummyState = { }
196
-
197
- subscription . onStateChange = function onStateChange ( ) {
198
- this . selector . run ( this . props )
199
-
200
- if ( ! this . selector . shouldComponentUpdate ) {
201
- subscription . notifyNestedSubs ( )
202
- } else {
203
- this . componentDidUpdate = function componentDidUpdate ( ) {
204
- this . componentDidUpdate = undefined
205
- subscription . notifyNestedSubs ( )
206
- }
207
-
208
- this . setState ( dummyState )
209
- }
210
- } . bind ( this )
196
+ if ( ! shouldHandleStateChanges ) return
197
+
198
+ // parentSub's source should match where store came from: props vs. context. A component
199
+ // connected to the store via props shouldn't use subscription from context, or vice versa.
200
+ const parentSub = ( this . propsMode ? this . props : this . context ) [ subscriptionKey ]
201
+ this . subscription = new Subscription ( this . store , parentSub , this . onStateChange . bind ( this ) )
202
+
203
+ // `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
204
+ // the middle of the notification loop, where `this.subscription` will then be null. An
205
+ // extra null check every change can be avoided by copying the method onto `this` and then
206
+ // replacing it with a no-op on unmount. This can probably be avoided if Subscription's
207
+ // listeners logic is changed to not call listeners that have been unsubscribed in the
208
+ // middle of the notification loop.
209
+ this . notifyNestedSubs = this . subscription . notifyNestedSubs . bind ( this . subscription )
210
+ }
211
+
212
+ onStateChange ( ) {
213
+ this . selector . run ( this . props )
214
+
215
+ if ( ! this . selector . shouldComponentUpdate ) {
216
+ this . notifyNestedSubs ( )
217
+ } else {
218
+ this . componentDidUpdate = this . notifyNestedSubsOnComponentDidUpdate
219
+ this . setState ( dummyState )
211
220
}
221
+ }
222
+
223
+ notifyNestedSubsOnComponentDidUpdate ( ) {
224
+ // `componentDidUpdate` is conditionally implemented when `onStateChange` determines it
225
+ // needs to notify nested subs. Once called, it unimplements itself until further state
226
+ // changes occur. Doing it this way vs having a permanent `componentDidMount` that does
227
+ // a boolean check every time avoids an extra method call most of the time, resulting
228
+ // in some perf boost.
229
+ this . componentDidUpdate = undefined
230
+ this . notifyNestedSubs ( )
212
231
}
213
232
214
233
isSubscribed ( ) {
215
234
return Boolean ( this . subscription ) && this . subscription . isSubscribed ( )
216
235
}
217
236
218
237
addExtraProps ( props ) {
219
- if ( ! withRef && ! renderCountProp ) return props
238
+ if ( ! withRef && ! renderCountProp && ! ( this . propsMode && this . subscription ) ) return props
220
239
// make a shallow copy so that fields added don't leak to the original selector.
221
240
// this is especially important for 'ref' since that's a reference back to the component
222
241
// instance. a singleton memoized selector would then be holding a reference to the
223
242
// instance, preventing the instance from being garbage collected, and that would be bad
224
243
const withExtras = { ...props }
225
244
if ( withRef ) withExtras . ref = this . setWrappedInstance
226
245
if ( renderCountProp ) withExtras [ renderCountProp ] = this . renderCount ++
246
+ if ( this . propsMode && this . subscription ) withExtras [ subscriptionKey ] = this . subscription
227
247
return withExtras
228
248
}
229
249
0 commit comments