@@ -6,6 +6,30 @@ import Subscription from '../utils/Subscription'
66import storeShape from '../utils/storeShape'
77
88let 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+
933export default function connectAdvanced (
1034 /*
1135 selectorFactory is a func that is responsible for returning the selector function used to
@@ -96,28 +120,27 @@ export default function connectAdvanced(
96120 this . version = version
97121 this . state = { }
98122 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 ] )
102125 this . setWrappedInstance = this . setWrappedInstance . bind ( this )
103126
104127 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>, ` +
108130 `or explicitly pass "${ storeKey } " as a prop to "${ displayName } ".`
109131 )
110132
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-
115133 this . initSelector ( )
116134 this . initSubscription ( )
117135 }
118136
119137 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 ] }
121144 }
122145
123146 componentDidMount ( ) {
@@ -144,12 +167,11 @@ export default function connectAdvanced(
144167
145168 componentWillUnmount ( ) {
146169 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
149170 this . subscription = null
171+ this . notifyNestedSubs = noop
150172 this . store = null
151- this . parentSub = null
152- this . selector . run = ( ) => { }
173+ this . selector . run = noop
174+ this . selector . shouldComponentUpdate = false
153175 }
154176
155177 getWrappedInstance ( ) {
@@ -165,65 +187,63 @@ export default function connectAdvanced(
165187 }
166188
167189 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 )
190193 }
191194
192195 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 )
211220 }
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 ( )
212231 }
213232
214233 isSubscribed ( ) {
215234 return Boolean ( this . subscription ) && this . subscription . isSubscribed ( )
216235 }
217236
218237 addExtraProps ( props ) {
219- if ( ! withRef && ! renderCountProp ) return props
238+ if ( ! withRef && ! renderCountProp && ! ( this . propsMode && this . subscription ) ) return props
220239 // make a shallow copy so that fields added don't leak to the original selector.
221240 // this is especially important for 'ref' since that's a reference back to the component
222241 // instance. a singleton memoized selector would then be holding a reference to the
223242 // instance, preventing the instance from being garbage collected, and that would be bad
224243 const withExtras = { ...props }
225244 if ( withRef ) withExtras . ref = this . setWrappedInstance
226245 if ( renderCountProp ) withExtras [ renderCountProp ] = this . renderCount ++
246+ if ( this . propsMode && this . subscription ) withExtras [ subscriptionKey ] = this . subscription
227247 return withExtras
228248 }
229249
0 commit comments