Skip to content

Commit eb960c4

Browse files
authored
Merge branch 'pre-6.x' into master
2 parents b255a46 + 9f44bed commit eb960c4

File tree

5 files changed

+128
-70
lines changed

5 files changed

+128
-70
lines changed

package-lock.json

+11-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-4
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@
4646
"hoist-non-react-statics": "^2.5.5",
4747
"invariant": "^2.2.4",
4848
"loose-envify": "^1.1.0",
49-
"prop-types": "^15.6.1",
50-
"react-lifecycles-compat": "^3.0.0"
49+
"prop-types": "^15.6.1"
5150
},
5251
"devDependencies": {
5352
"babel-cli": "^6.26.0",
@@ -89,8 +88,6 @@
8988
"jest": "^23.4.1",
9089
"jest-dom": "^1.12.0",
9190
"npm-run": "^5.0.1",
92-
"react": "^16.3.2",
93-
"react-dom": "^16.3.2",
9491
"react-testing-library": "^5.0.0",
9592
"redux": "^4.0.0",
9693
"rimraf": "^2.6.2",

src/components/connectAdvanced.js

+65-48
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,12 @@
11
import hoistStatics from 'hoist-non-react-statics'
22
import invariant from 'invariant'
33
import { Component, createElement } from 'react'
4-
import { polyfill } from 'react-lifecycles-compat'
54

65
import Subscription from '../utils/Subscription'
76
import { storeShape, subscriptionShape } from '../utils/PropTypes'
87

98
let hotReloadingVersion = 0
109
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,
20-
}
21-
}
22-
return {
23-
shouldComponentUpdate: false,
24-
}
25-
} catch (error) {
26-
return {
27-
shouldComponentUpdate: true,
28-
error,
29-
}
30-
}
31-
}
32-
}
3310

3411
export default function connectAdvanced(
3512
/*
@@ -88,10 +65,6 @@ export default function connectAdvanced(
8865
[subscriptionKey]: subscriptionShape,
8966
}
9067

91-
function getDerivedStateFromProps(nextProps, prevState) {
92-
return prevState.updater(nextProps, prevState)
93-
}
94-
9568
return function wrapWithConnect(WrappedComponent) {
9669
invariant(
9770
typeof WrappedComponent == 'function',
@@ -134,10 +107,14 @@ export default function connectAdvanced(
134107
`or explicitly pass "${storeKey}" as a prop to "${displayName}".`
135108
)
136109

110+
this.createSelector()
137111
this.state = {
138-
updater: this.createUpdater()
112+
updateCount: 0
139113
}
114+
this.storeState = this.store.getState()
140115
this.initSubscription()
116+
this.derivedProps = this.derivedPropsUpdater()
117+
this.received = this.props
141118
}
142119

143120
getChildContext() {
@@ -159,11 +136,17 @@ export default function connectAdvanced(
159136
// dispatching an action in its componentWillMount, we have to re-run the select and maybe
160137
// re-render.
161138
this.subscription.trySubscribe()
162-
this.runUpdater()
139+
this.triggerUpdateOnStoreStateChange()
163140
}
164141

165-
shouldComponentUpdate(_, nextState) {
166-
return nextState.shouldComponentUpdate
142+
shouldComponentUpdate(nextProps) {
143+
this.received = nextProps
144+
// received a prop update, store state updates are handled in onStateChange
145+
const oldProps = this.derivedProps
146+
const newProps = this.updateDerivedProps(nextProps)
147+
if (this.error) return true
148+
const sCU = newProps !== oldProps
149+
return sCU
167150
}
168151

169152
componentWillUnmount() {
@@ -174,6 +157,31 @@ export default function connectAdvanced(
174157
this.isUnmounted = true
175158
}
176159

160+
updateDerivedProps(nextProps) {
161+
this.derivedProps = this.derivedPropsUpdater(nextProps)
162+
return this.derivedProps
163+
}
164+
165+
derivedPropsUpdater(props = this.props) {
166+
// runs when props change, or the store state changes
167+
// and generates the derived props for connected components
168+
try {
169+
const nextProps = this.sourceSelector(this.storeState, props)
170+
if (nextProps !== this.derivedProps || this.error) {
171+
this.error = null
172+
return nextProps
173+
}
174+
return this.derivedProps
175+
} catch (error) {
176+
this.error = error
177+
return this.derivedProps
178+
}
179+
}
180+
181+
createSelector() {
182+
this.sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
183+
}
184+
177185
getWrappedInstance() {
178186
invariant(withRef,
179187
`To access the wrapped instance, you need to specify ` +
@@ -186,17 +194,24 @@ export default function connectAdvanced(
186194
this.wrappedInstance = ref
187195
}
188196

189-
createUpdater() {
190-
const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
191-
return makeUpdater(sourceSelector, this.store)
192-
}
193-
194-
runUpdater(callback = noop) {
197+
triggerUpdateOnStoreStateChange(callback = noop) {
198+
// runs when an action is dispatched by the store we are listening to
199+
// if the store state has changed, we save that and update the component state
200+
// to force a re-generation of derived props
195201
if (this.isUnmounted) {
196202
return
197203
}
198204

199-
this.setState(prevState => prevState.updater(this.props, prevState), callback)
205+
this.setState(prevState => {
206+
const newState = this.store.getState()
207+
if (this.storeState === newState) {
208+
return prevState
209+
}
210+
this.storeState = newState
211+
return {
212+
updateCount: prevState.updateCount++
213+
}
214+
}, callback)
200215
}
201216

202217
initSubscription() {
@@ -217,7 +232,7 @@ export default function connectAdvanced(
217232
}
218233

219234
onStateChange() {
220-
this.runUpdater(this.notifyNestedSubs)
235+
this.triggerUpdateOnStoreStateChange(this.notifyNestedSubs)
221236
}
222237

223238
isSubscribed() {
@@ -238,10 +253,16 @@ export default function connectAdvanced(
238253
}
239254

240255
render() {
241-
if (this.state.error) {
242-
throw this.state.error
256+
if (this.received !== this.props) {
257+
// forceUpdate() was called on this component, which skips sCU
258+
// so manually update derived props
259+
this.received = this.props
260+
this.updateDerivedProps(this.props)
261+
}
262+
if (this.error) {
263+
throw this.error
243264
} else {
244-
return createElement(WrappedComponent, this.addExtraProps(this.state.props))
265+
return createElement(WrappedComponent, this.addExtraProps(this.derivedProps))
245266
}
246267
}
247268
}
@@ -251,7 +272,6 @@ export default function connectAdvanced(
251272
Connect.childContextTypes = childContextTypes
252273
Connect.contextTypes = contextTypes
253274
Connect.propTypes = contextTypes
254-
Connect.getDerivedStateFromProps = getDerivedStateFromProps
255275

256276
if (process.env.NODE_ENV !== 'production') {
257277
Connect.prototype.componentDidUpdate = function componentDidUpdate() {
@@ -276,15 +296,12 @@ export default function connectAdvanced(
276296
oldListeners.forEach(listener => this.subscription.listeners.subscribe(listener))
277297
}
278298

279-
const updater = this.createUpdater()
280-
this.setState({updater})
281-
this.runUpdater()
299+
this.createSelector()
300+
this.triggerUpdateOnStoreStateChange()
282301
}
283302
}
284303
}
285304

286-
polyfill(Connect)
287-
288305
return hoistStatics(Connect, WrappedComponent)
289306
}
290307
}

test/components/Provider.spec.js

+20-3
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,12 @@ describe('React', () => {
214214
}
215215
}
216216

217+
const childCalls = []
217218
@connect((state, parentProps) => {
218219
childMapStateInvokes++
220+
childCalls.push([state, parentProps.parentState])
219221
// The state from parent props should always be consistent with the current state
220-
expect(state).toEqual(parentProps.parentState)
222+
//expect(state).toEqual(parentProps.parentState)
221223
return {}
222224
})
223225
class ChildContainer extends Component {
@@ -236,16 +238,31 @@ describe('React', () => {
236238

237239
// The store state stays consistent when setState calls are batched
238240
store.dispatch({ type: 'APPEND', body: 'c' })
239-
expect(childMapStateInvokes).toBe(2)
241+
expect(childMapStateInvokes).toBe(3)
242+
expect(childCalls).toEqual([
243+
['a', 'a'],
244+
['a', 'ac'], // parent updates first, passes props
245+
['ac', 'ac'] // then store update is processed
246+
])
240247

241248
// setState calls DOM handlers are batched
249+
242250
const button = tester.getByText('change')
243251
rtl.fireEvent.click(button)
244252
expect(childMapStateInvokes).toBe(3)
245253

246254
// Provider uses unstable_batchedUpdates() under the hood
247255
store.dispatch({ type: 'APPEND', body: 'd' })
248-
expect(childMapStateInvokes).toBe(4)
256+
expect(childCalls).toEqual([
257+
['a', 'a'],
258+
['a', 'ac'], // parent updates first, passes props
259+
['ac', 'ac'], // then store update is processed
260+
['ac', 'acb'], // parent updates first, passes props
261+
['acb', 'acb'], // then store update is processed
262+
['acb', 'acbd'], // parent updates first, passes props
263+
['acbd', 'acbd'], // then store update is processed
264+
])
265+
expect(childMapStateInvokes).toBe(7)
249266
})
250267

251268
it('works in <StrictMode> without warnings (React 16.3+)', () => {

test/components/connect.spec.js

+31-5
Original file line numberDiff line numberDiff line change
@@ -1773,10 +1773,12 @@ describe('React', () => {
17731773
}
17741774
}
17751775

1776+
const childCalls = []
17761777
@connect((state, parentProps) => {
17771778
childMapStateInvokes++
1779+
childCalls.push([state, parentProps.parentState])
17781780
// The state from parent props should always be consistent with the current state
1779-
expect(state).toEqual(parentProps.parentState)
1781+
//expect(state).toEqual(parentProps.parentState)
17801782
return {}
17811783
})
17821784
class ChildContainer extends Component {
@@ -1792,20 +1794,37 @@ describe('React', () => {
17921794
)
17931795

17941796
expect(childMapStateInvokes).toBe(1)
1797+
expect(childCalls).toEqual([
1798+
['a', 'a']
1799+
])
17951800

17961801
// The store state stays consistent when setState calls are batched
17971802
ReactDOM.unstable_batchedUpdates(() => {
17981803
store.dispatch({ type: 'APPEND', body: 'c' })
17991804
})
1800-
expect(childMapStateInvokes).toBe(2)
1805+
expect(childMapStateInvokes).toBe(3)
1806+
expect(childCalls).toEqual([
1807+
['a', 'a'],
1808+
['a', 'ac'],
1809+
['ac', 'ac'],
1810+
])
18011811

18021812
// setState calls DOM handlers are batched
18031813
const button = tester.getByText('change')
18041814
rtl.fireEvent.click(button)
18051815
expect(childMapStateInvokes).toBe(3)
18061816

18071817
store.dispatch({ type: 'APPEND', body: 'd' })
1808-
expect(childMapStateInvokes).toBe(4)
1818+
expect(childMapStateInvokes).toBe(7)
1819+
expect(childCalls).toEqual([
1820+
['a', 'a'],
1821+
['a', 'ac'],
1822+
['ac', 'ac'],
1823+
['ac', 'acb'],
1824+
['acb', 'acb'],
1825+
['acb', 'acbd'],
1826+
['acbd', 'acbd'],
1827+
])
18091828
})
18101829

18111830
it('should not render the wrapped component when mapState does not produce change', () => {
@@ -2021,7 +2040,7 @@ describe('React', () => {
20212040
return { ...stateProps, ...dispatchProps, name: parentProps.name }
20222041
}
20232042

2024-
@connect(null, mapDispatchFactory, mergeParentDispatch)
2043+
@connect(() => ({}), mapDispatchFactory, mergeParentDispatch)
20252044
class Passthrough extends Component {
20262045
componentDidUpdate() {
20272046
updatedCount++
@@ -2281,8 +2300,9 @@ describe('React', () => {
22812300
@connect() // no mapStateToProps. therefore it should be transparent for subscriptions
22822301
class B extends React.Component { render() { return <C {...this.props} /> }}
22832302

2303+
let calls = []
22842304
@connect((state, props) => {
2285-
expect(props.count).toBe(state)
2305+
calls.push([state, props.count])
22862306
return { count: state * 10 + props.count }
22872307
})
22882308
class C extends React.Component { render() { return <div>{this.props.count}</div> }}
@@ -2291,6 +2311,12 @@ describe('React', () => {
22912311
rtl.render(<ProviderMock store={store}><A /></ProviderMock>)
22922312

22932313
store.dispatch({ type: 'INC' })
2314+
2315+
expect(calls).toEqual([
2316+
[0, 0],
2317+
[0, 1], // props updates first
2318+
[1, 1], // then state
2319+
])
22942320
})
22952321

22962322
it('should subscribe properly when a new store is provided via props', () => {

0 commit comments

Comments
 (0)