Skip to content

Commit e9a874f

Browse files
committed
Almost-working refactor of connect. Just 1 test not passing: the hard one :'(
1 parent ab1c422 commit e9a874f

File tree

2 files changed

+134
-143
lines changed

2 files changed

+134
-143
lines changed

src/components/connect.js

Lines changed: 116 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,22 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
7373
}
7474

7575
class Connect extends Component {
76-
shouldComponentUpdate() {
77-
return !pure || this.haveOwnPropsChanged || this.hasStoreStateChanged
76+
77+
shouldComponentUpdate(nextProps,nextState) {
78+
79+
if ( this.skipNextRender ) {
80+
this.skipNextRender = false;
81+
return false;
82+
}
83+
84+
/*
85+
console.log("shouldComponentUpdate",!pure || nextState.mergedPropsUpdated)
86+
console.log("shouldComponentUpdate",!pure || nextState.mergedPropsUpdated)
87+
console.log("shouldComponentUpdate",nextState)
88+
console.log("shouldComponentUpdate nextState.mergedPropsUpdated",nextState.mergedPropsUpdated)
89+
*/
90+
console.log("################## shouldComponentUpdate",nextState.mergedPropsUpdated)
91+
return !pure || nextState.mergedPropsUpdated
7892
}
7993

8094
constructor(props, context) {
@@ -88,9 +102,7 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
88102
`Either wrap the root component in a <Provider>, ` +
89103
`or explicitly pass "store" as a prop to "${connectDisplayName}".`
90104
)
91-
92-
const storeState = this.store.getState()
93-
this.state = { storeState }
105+
this.state = { }
94106
this.clearCache()
95107
}
96108

@@ -152,45 +164,21 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
152164
return dispatchProps
153165
}
154166

155-
updateStatePropsIfNeeded() {
156-
const nextStateProps = this.computeStateProps(this.props)
157-
if (this.stateProps && shallowEqual(nextStateProps, this.stateProps)) {
158-
return false
159-
}
160-
161-
this.stateProps = nextStateProps
162-
return true
163-
}
164-
165-
updateDispatchPropsIfNeeded() {
166-
const nextDispatchProps = this.computeDispatchProps(this.props)
167-
if (this.dispatchProps && shallowEqual(nextDispatchProps, this.dispatchProps)) {
168-
return false
169-
}
170-
171-
this.dispatchProps = nextDispatchProps
172-
return true
173-
}
174-
175-
updateMergedPropsIfNeeded() {
176-
const nextMergedProps = computeMergedProps(this.stateProps, this.dispatchProps, this.props)
177-
if (this.mergedProps && checkMergedEquals && shallowEqual(nextMergedProps, this.mergedProps)) {
178-
return false
179-
}
180-
181-
this.mergedProps = nextMergedProps
182-
return true
183-
}
184-
185167
isSubscribed() {
186168
return typeof this.unsubscribe === 'function'
187169
}
188170

189171
trySubscribe() {
172+
console.log("trySubscribe")
190173
if (shouldSubscribe && !this.unsubscribe) {
191-
this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
192-
this.handleChange()
174+
this.unsubscribe = this.store.subscribe(() => {
175+
if (!this.unsubscribe) {
176+
return
177+
}
178+
this.handleChange(false,false,true)
179+
})
193180
}
181+
this.handleChange(true,false,false)
194182
}
195183

196184
tryUnsubscribe() {
@@ -200,13 +188,22 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
200188
}
201189
}
202190

203-
componentDidMount() {
191+
componentWillMount() {
204192
this.trySubscribe()
205193
}
206194

207195
componentWillReceiveProps(nextProps) {
208-
if (!pure || !shallowEqual(nextProps, this.props)) {
209-
this.haveOwnPropsChanged = true
196+
if (pure) {
197+
const propsUpdated = !shallowEqual(nextProps, this.props)
198+
if ( propsUpdated ) {
199+
this.handleChange(false,true,false)
200+
}
201+
else {
202+
this.skipNextRender = true;
203+
}
204+
}
205+
else {
206+
this.handleChange(false,true,false)
210207
}
211208
}
212209

@@ -216,120 +213,111 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
216213
}
217214

218215
clearCache() {
219-
this.dispatchProps = null
220-
this.stateProps = null
221-
this.mergedProps = null
222-
this.haveOwnPropsChanged = true
223-
this.hasStoreStateChanged = true
224-
this.haveStatePropsBeenPrecalculated = false
225-
this.statePropsPrecalculationError = null
226-
this.renderedElement = null
227216
this.finalMapDispatchToProps = null
228217
this.finalMapStateToProps = null
229218
}
230219

231-
handleChange() {
232-
if (!this.unsubscribe) {
233-
return
220+
handleChange(isInit,isPropsChange,isStateChange) {
221+
const result = tryCatch(() => this.doHandleChange(isInit,isPropsChange,isStateChange))
222+
if ( result === errorObject ) {
223+
this.setState({error: errorObject.value})
234224
}
225+
}
226+
227+
// TODO can we have a better method signature? it seems spread does not work?
228+
doHandleChange(isInit,isPropsChange,isStateChange) {
229+
console.log("################## handleChange for "+(isInit ? "init" : isPropsChange ? "props" : "state"))
235230

236231
const storeState = this.store.getState()
237-
const prevStoreState = this.state.storeState
238-
if (pure && prevStoreState === storeState) {
232+
if (isStateChange && pure && (storeState === this.state.storeState)) {
233+
console.log("handleChange same state: return")
239234
return
240235
}
241236

242-
if (pure && !this.doStatePropsDependOnOwnProps) {
243-
const haveStatePropsChanged = tryCatch(this.updateStatePropsIfNeeded, this)
244-
if (!haveStatePropsChanged) {
245-
return
237+
// This is put outside because it's used in both the fast and normal paths
238+
let stateProps
239+
let statePropsUpdated
240+
const setStatePropsIfNeeded = (props) => {
241+
if (typeof statePropsUpdated === 'undefined') {
242+
stateProps = (!isInit && pure && isPropsChange && !this.doStatePropsDependOnOwnProps) ?
243+
this.state.stateProps :
244+
this.computeStateProps(props)
245+
statePropsUpdated = !this.state.stateProps || !shallowEqual(stateProps, this.state.stateProps)
246+
console.log("stateProps",stateProps)
247+
console.log("statePropsUpdated",statePropsUpdated)
246248
}
247-
if (haveStatePropsChanged === errorObject) {
248-
this.statePropsPrecalculationError = errorObject.value
249+
}
250+
251+
252+
// Fast track: bailout early because we don't need access to fresh props here
253+
if (isStateChange && pure && !this.doStatePropsDependOnOwnProps) {
254+
setStatePropsIfNeeded(this.props)
255+
if (!statePropsUpdated) {
256+
console.log("-> fast track return")
257+
return
249258
}
250-
this.haveStatePropsBeenPrecalculated = true
251259
}
252260

253-
this.hasStoreStateChanged = true
254-
this.setState({ storeState })
261+
262+
// Normal track: we precompute everything for shouldComponentUpdate, and try to reuse already done computation
263+
this.setState((previousState, currentProps) => {
264+
console.log("-> normal track with props=",currentProps)
265+
266+
setStatePropsIfNeeded(currentProps);
267+
const dispatchProps = (!isInit && pure && isPropsChange && !this.doDispatchPropsDependOnOwnProps) ?
268+
this.state.dispatchProps :
269+
this.computeDispatchProps(currentProps)
270+
const dispatchPropsUpdated = !this.state.dispatchProps || !shallowEqual(dispatchProps, this.state.dispatchProps)
271+
console.log("dispatchProps",dispatchProps)
272+
console.log("dispatchPropsUpdated",dispatchPropsUpdated)
273+
274+
const mergedProps = (statePropsUpdated || dispatchPropsUpdated || isPropsChange) ?
275+
computeMergedProps(stateProps, dispatchProps, currentProps) :
276+
this.state.mergedProps
277+
const mergedPropsUpdated = !this.state.mergedProps || !shallowEqual(mergedProps, this.state.mergedProps)
278+
console.log("mergedProps",mergedProps)
279+
console.log("mergedPropsUpdated",mergedPropsUpdated)
280+
281+
282+
return {
283+
error: undefined,
284+
storeState,
285+
stateProps,
286+
dispatchProps,
287+
mergedProps,
288+
mergedPropsUpdated
289+
}
290+
});
255291
}
256292

257293
getWrappedInstance() {
258294
invariant(withRef,
259295
`To access the wrapped instance, you need to specify ` +
260296
`{ withRef: true } as the fourth argument of the connect() call.`
261297
)
262-
263298
return this.refs.wrappedInstance
264299
}
265300

266-
render() {
267-
const {
268-
haveOwnPropsChanged,
269-
hasStoreStateChanged,
270-
haveStatePropsBeenPrecalculated,
271-
statePropsPrecalculationError,
272-
renderedElement
273-
} = this
274-
275-
this.haveOwnPropsChanged = false
276-
this.hasStoreStateChanged = false
277-
this.haveStatePropsBeenPrecalculated = false
278-
this.statePropsPrecalculationError = null
279-
280-
if (statePropsPrecalculationError) {
281-
throw statePropsPrecalculationError
282-
}
283-
284-
let shouldUpdateStateProps = true
285-
let shouldUpdateDispatchProps = true
286-
if (pure && renderedElement) {
287-
shouldUpdateStateProps = hasStoreStateChanged || (
288-
haveOwnPropsChanged && this.doStatePropsDependOnOwnProps
289-
)
290-
shouldUpdateDispatchProps =
291-
haveOwnPropsChanged && this.doDispatchPropsDependOnOwnProps
292-
}
293-
294-
let haveStatePropsChanged = false
295-
let haveDispatchPropsChanged = false
296-
if (haveStatePropsBeenPrecalculated) {
297-
haveStatePropsChanged = true
298-
} else if (shouldUpdateStateProps) {
299-
haveStatePropsChanged = this.updateStatePropsIfNeeded()
300-
}
301-
if (shouldUpdateDispatchProps) {
302-
haveDispatchPropsChanged = this.updateDispatchPropsIfNeeded()
303-
}
304-
305-
let haveMergedPropsChanged = true
306-
if (
307-
haveStatePropsChanged ||
308-
haveDispatchPropsChanged ||
309-
haveOwnPropsChanged
310-
) {
311-
haveMergedPropsChanged = this.updateMergedPropsIfNeeded()
312-
} else {
313-
haveMergedPropsChanged = false
301+
getRenderedElementProps() {
302+
if ( withRef ) {
303+
return {
304+
...this.state.mergedProps,
305+
ref: 'wrappedInstance'
306+
}
314307
}
315-
316-
if (!haveMergedPropsChanged && renderedElement) {
317-
return renderedElement
308+
else {
309+
return this.state.mergedProps
318310
}
311+
}
319312

320-
if (withRef) {
321-
this.renderedElement = createElement(WrappedComponent, {
322-
...this.mergedProps,
323-
ref: 'wrappedInstance'
324-
})
325-
} else {
326-
this.renderedElement = createElement(WrappedComponent,
327-
this.mergedProps
328-
)
313+
render() {
314+
console.log("################## render")
315+
if ( this.state.error ) {
316+
throw this.state.error;
329317
}
330-
331-
return this.renderedElement
318+
return createElement(WrappedComponent,this.getRenderedElementProps())
332319
}
320+
333321
}
334322

335323
Connect.displayName = connectDisplayName
@@ -349,8 +337,10 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
349337

350338
// We are hot reloading!
351339
this.version = version
352-
this.trySubscribe()
353340
this.clearCache()
341+
this.setState({},() => {
342+
this.trySubscribe()
343+
})
354344
}
355345
}
356346

0 commit comments

Comments
 (0)