1
1
import hoistStatics from 'hoist-non-react-statics'
2
2
import invariant from 'invariant'
3
- import { Component , createElement } from 'react'
4
- import { polyfill } from 'react-lifecycles-compat'
3
+ import React , { Component , createElement } from 'react'
5
4
6
- import Subscription from '../utils/Subscription'
7
- import { storeShape , subscriptionShape } from '../utils/PropTypes'
5
+ import { ReactReduxContext } from "./context" ;
6
+ import { storeShape } from '../utils/PropTypes'
8
7
9
8
let hotReloadingVersion = 0
9
+ const dummyState = { }
10
10
function noop ( ) { }
11
- function makeUpdater ( sourceSelector , store ) {
12
- return function updater ( props , prevState ) {
13
- try {
14
- const nextProps = sourceSelector ( store . getState ( ) , props )
15
- if ( nextProps !== prevState . props || prevState . error ) {
16
- return {
17
- shouldComponentUpdate : true ,
18
- props : nextProps ,
19
- error : null ,
11
+ function makeSelectorStateful ( sourceSelector ) {
12
+ // wrap the selector in an object that tracks its results between runs.
13
+ const selector = {
14
+ run : function runComponentSelector ( props , storeState ) {
15
+ try {
16
+ const nextProps = sourceSelector ( storeState , props )
17
+ if ( nextProps !== selector . props || selector . error ) {
18
+ selector . shouldComponentUpdate = true
19
+ selector . props = nextProps
20
+ selector . error = null
20
21
}
21
- }
22
- return {
23
- shouldComponentUpdate : false ,
24
- }
25
- } catch ( error ) {
26
- return {
27
- shouldComponentUpdate : true ,
28
- error,
22
+ } catch ( error ) {
23
+ selector . shouldComponentUpdate = true
24
+ selector . error = error
29
25
}
30
26
}
31
27
}
28
+
29
+ return selector
32
30
}
33
31
34
32
export default function connectAdvanced (
@@ -77,24 +75,12 @@ export default function connectAdvanced(
77
75
...connectOptions
78
76
} = { }
79
77
) {
80
- const subscriptionKey = storeKey + 'Subscription'
81
78
const version = hotReloadingVersion ++
82
79
83
- const contextTypes = {
84
- [ storeKey ] : storeShape ,
85
- [ subscriptionKey ] : subscriptionShape ,
86
- }
87
- const childContextTypes = {
88
- [ subscriptionKey ] : subscriptionShape ,
89
- }
90
-
91
- function getDerivedStateFromProps ( nextProps , prevState ) {
92
- return prevState . updater ( nextProps , prevState )
93
- }
94
80
95
81
return function wrapWithConnect ( WrappedComponent ) {
96
82
invariant (
97
- typeof WrappedComponent == 'function' ,
83
+ typeof WrappedComponent === 'function' ,
98
84
`You must pass a component to the function returned by ` +
99
85
`${ methodName } . Instead received ${ JSON . stringify ( WrappedComponent ) } `
100
86
)
@@ -119,34 +105,25 @@ export default function connectAdvanced(
119
105
}
120
106
121
107
class Connect extends Component {
122
- constructor ( props , context ) {
123
- super ( props , context )
108
+ constructor ( props ) {
109
+ super ( props )
124
110
125
111
this . version = version
126
112
this . renderCount = 0
127
- this . store = props [ storeKey ] || context [ storeKey ]
128
- this . propsMode = Boolean ( props [ storeKey ] )
113
+ this . storeState = null ;
114
+
115
+
129
116
this . setWrappedInstance = this . setWrappedInstance . bind ( this )
117
+ this . renderChild = this . renderChild . bind ( this ) ;
130
118
119
+ // TODO How do we express the invariant of needing a Provider when it's used in render()?
120
+ /*
131
121
invariant(this.store,
132
122
`Could not find "${storeKey}" in either the context or props of ` +
133
123
`"${displayName}". Either wrap the root component in a <Provider>, ` +
134
124
`or explicitly pass "${storeKey}" as a prop to "${displayName}".`
135
125
)
136
-
137
- this . state = {
138
- updater : this . createUpdater ( )
139
- }
140
- this . initSubscription ( )
141
- }
142
-
143
- getChildContext ( ) {
144
- // If this component received store from props, its subscription should be transparent
145
- // to any descendants receiving store+subscription from context; it passes along
146
- // subscription passed to it. Otherwise, it shadows the parent subscription, which allows
147
- // Connect to control ordering of notifications to flow top-down.
148
- const subscription = this . propsMode ? null : this . subscription
149
- return { [ subscriptionKey ] : subscription || this . context [ subscriptionKey ] }
126
+ */
150
127
}
151
128
152
129
componentDidMount ( ) {
@@ -158,20 +135,24 @@ export default function connectAdvanced(
158
135
// To handle the case where a child component may have triggered a state change by
159
136
// dispatching an action in its componentWillMount, we have to re-run the select and maybe
160
137
// re-render.
161
- this . subscription . trySubscribe ( )
162
- this . runUpdater ( )
138
+ this . selector . run ( this . props , this . storeState ) ;
139
+ if ( this . selector . shouldComponentUpdate ) this . forceUpdate ( )
140
+ }
141
+
142
+
143
+ UNSAFE_componentWillReceiveProps ( nextProps ) {
144
+ // TODO Do we still want/need to implement sCU / cWRP now?
145
+ this . selector . run ( nextProps , this . storeState ) ;
163
146
}
164
147
165
- shouldComponentUpdate ( _ , nextState ) {
166
- return nextState . shouldComponentUpdate
148
+ shouldComponentUpdate ( ) {
149
+ return this . selector . shouldComponentUpdate
167
150
}
168
151
152
+
169
153
componentWillUnmount ( ) {
170
- if ( this . subscription ) this . subscription . tryUnsubscribe ( )
171
- this . subscription = null
172
- this . notifyNestedSubs = noop
173
- this . store = null
174
- this . isUnmounted = true
154
+ this . selector . run = noop
155
+ this . selector . shouldComponentUpdate = false
175
156
}
176
157
177
158
getWrappedInstance ( ) {
@@ -186,78 +167,77 @@ export default function connectAdvanced(
186
167
this . wrappedInstance = ref
187
168
}
188
169
189
- createUpdater ( ) {
190
- const sourceSelector = selectorFactory ( this . store . dispatch , selectorFactoryOptions )
191
- return makeUpdater ( sourceSelector , this . store )
192
- }
193
-
194
- runUpdater ( callback = noop ) {
195
- if ( this . isUnmounted ) {
196
- return
197
- }
198
-
199
- this . setState ( prevState => prevState . updater ( this . props , prevState ) , callback )
200
- }
201
-
202
- initSubscription ( ) {
203
- if ( ! shouldHandleStateChanges ) return
204
-
205
- // parentSub's source should match where store came from: props vs. context. A component
206
- // connected to the store via props shouldn't use subscription from context, or vice versa.
207
- const parentSub = ( this . propsMode ? this . props : this . context ) [ subscriptionKey ]
208
- this . subscription = new Subscription ( this . store , parentSub , this . onStateChange . bind ( this ) )
209
-
210
- // `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
211
- // the middle of the notification loop, where `this.subscription` will then be null. An
212
- // extra null check every change can be avoided by copying the method onto `this` and then
213
- // replacing it with a no-op on unmount. This can probably be avoided if Subscription's
214
- // listeners logic is changed to not call listeners that have been unsubscribed in the
215
- // middle of the notification loop.
216
- this . notifyNestedSubs = this . subscription . notifyNestedSubs . bind ( this . subscription )
217
- }
218
-
219
- onStateChange ( ) {
220
- this . runUpdater ( this . notifyNestedSubs )
221
- }
222
-
223
- isSubscribed ( ) {
224
- return Boolean ( this . subscription ) && this . subscription . isSubscribed ( )
170
+ initSelector ( dispatch , storeState ) {
171
+ const sourceSelector = selectorFactory ( dispatch , selectorFactoryOptions )
172
+ this . selector = makeSelectorStateful ( sourceSelector )
173
+ this . selector . run ( this . props , storeState ) ;
225
174
}
226
175
227
176
addExtraProps ( props ) {
228
- if ( ! withRef && ! renderCountProp && ! ( this . propsMode && this . subscription ) ) return props
177
+ if ( ! withRef && ! renderCountProp ) return props ;
178
+
229
179
// make a shallow copy so that fields added don't leak to the original selector.
230
180
// this is especially important for 'ref' since that's a reference back to the component
231
181
// instance. a singleton memoized selector would then be holding a reference to the
232
182
// instance, preventing the instance from being garbage collected, and that would be bad
233
183
const withExtras = { ...props }
234
184
if ( withRef ) withExtras . ref = this . setWrappedInstance
235
185
if ( renderCountProp ) withExtras [ renderCountProp ] = this . renderCount ++
236
- if ( this . propsMode && this . subscription ) withExtras [ subscriptionKey ] = this . subscription
186
+
237
187
return withExtras
238
188
}
239
189
190
+ renderChild ( providerValue ) {
191
+ const { storeState, dispatch} = providerValue ;
192
+
193
+ this . storeState = storeState ;
194
+
195
+ if ( this . selector ) {
196
+ this . selector . run ( this . props , storeState ) ;
197
+ }
198
+ else {
199
+ this . initSelector ( dispatch , storeState ) ;
200
+ }
201
+
202
+ if ( this . selector . error ) {
203
+ // TODO This will unmount the whole tree now that we're throwing in render. Good idea?
204
+ // TODO Related: https://github.com/reactjs/react-redux/issues/802
205
+ throw this . selector . error
206
+ }
207
+ else if ( this . selector . shouldComponentUpdate ) {
208
+ //console.log(`Re-rendering component (${displayName})`, this.selector.props);
209
+ this . selector . shouldComponentUpdate = false ;
210
+ this . renderedElement = createElement ( WrappedComponent , this . addExtraProps ( this . selector . props ) ) ;
211
+ }
212
+ else {
213
+ //console.log(`Returning existing render result (${displayName})`, this.props)
214
+ }
215
+
216
+ return this . renderedElement ;
217
+ }
218
+
240
219
render ( ) {
241
- if ( this . state . error ) {
242
- throw this . state . error
243
- } else {
244
- return createElement ( WrappedComponent , this . addExtraProps ( this . state . props ) )
245
- }
220
+ return (
221
+ < ReactReduxContext . Consumer >
222
+ { this . renderChild }
223
+ </ ReactReduxContext . Consumer >
224
+ )
246
225
}
247
226
}
248
227
249
228
Connect . WrappedComponent = WrappedComponent
250
229
Connect . displayName = displayName
251
- Connect . childContextTypes = childContextTypes
252
- Connect . contextTypes = contextTypes
253
- Connect . propTypes = contextTypes
254
- Connect . getDerivedStateFromProps = getDerivedStateFromProps
230
+ // TODO We're losing the ability to add a store as a prop. Not sure there's anything we can do about that.
231
+ //Connect.propTypes = contextTypes
255
232
233
+ // TODO With connect no longer managing subscriptions, I _think_ is is all unneeded
234
+ /*
256
235
if (process.env.NODE_ENV !== 'production') {
257
- Connect . prototype . componentDidUpdate = function componentDidUpdate ( ) {
236
+ Connect.prototype.componentWillUpdate = function componentWillUpdate () {
258
237
// We are hot reloading!
259
238
if (this.version !== version) {
260
239
this.version = version
240
+ this.initSelector()
261
241
262
242
// If any connected descendants don't hot reload (and resubscribe in the process), their
263
243
// listeners will be lost when we unsubscribe. Unfortunately, by copying over all
@@ -275,15 +255,10 @@ export default function connectAdvanced(
275
255
this.subscription.trySubscribe()
276
256
oldListeners.forEach(listener => this.subscription.listeners.subscribe(listener))
277
257
}
278
-
279
- const updater = this . createUpdater ( )
280
- this . setState ( { updater} )
281
- this . runUpdater ( )
282
258
}
283
259
}
284
260
}
285
-
286
- polyfill ( Connect )
261
+ */
287
262
288
263
return hoistStatics ( Connect , WrappedComponent )
289
264
}
0 commit comments