Skip to content

Commit ea53d6f

Browse files
committed
Errors thrown on the fast path should be rethrown in render()
This fixes #351 because child render() will never run for dispatches after componentWillUnmount()
1 parent 85982f6 commit ea53d6f

File tree

2 files changed

+102
-1
lines changed

2 files changed

+102
-1
lines changed

src/components/connect.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ function getDisplayName(WrappedComponent) {
1919
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
2020
}
2121

22+
let errorObject = { value: null }
23+
function tryCatch(fn, ctx) {
24+
try {
25+
return fn.apply(ctx)
26+
} catch (e) {
27+
errorObject.value = e
28+
return errorObject
29+
}
30+
}
31+
2232
// Helps track hot reloading.
2333
let nextVersion = 0
2434

@@ -220,6 +230,7 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
220230
this.haveOwnPropsChanged = true
221231
this.hasStoreStateChanged = true
222232
this.haveStatePropsBeenPrecalculated = false
233+
this.statePropsPrecalculationError = null
223234
this.renderedElement = null
224235
this.finalMapDispatchToProps = null
225236
this.finalMapStateToProps = null
@@ -237,9 +248,13 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
237248
}
238249

239250
if (pure && !this.doStatePropsDependOnOwnProps) {
240-
if (!this.updateStatePropsIfNeeded()) {
251+
const haveStatePropsChanged = tryCatch(this.updateStatePropsIfNeeded, this)
252+
if (!haveStatePropsChanged) {
241253
return
242254
}
255+
if (haveStatePropsChanged === errorObject) {
256+
this.statePropsPrecalculationError = errorObject.value
257+
}
243258
this.haveStatePropsBeenPrecalculated = true
244259
}
245260

@@ -261,12 +276,18 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
261276
haveOwnPropsChanged,
262277
hasStoreStateChanged,
263278
haveStatePropsBeenPrecalculated,
279+
statePropsPrecalculationError,
264280
renderedElement
265281
} = this
266282

267283
this.haveOwnPropsChanged = false
268284
this.hasStoreStateChanged = false
269285
this.haveStatePropsBeenPrecalculated = false
286+
this.statePropsPrecalculationError = null
287+
288+
if (statePropsPrecalculationError) {
289+
throw statePropsPrecalculationError
290+
}
270291

271292
let shouldUpdateStateProps = true
272293
let shouldUpdateDispatchProps = true

test/components/connect.spec.js

+80
Original file line numberDiff line numberDiff line change
@@ -1626,6 +1626,39 @@ describe('React', () => {
16261626
spy.destroy()
16271627
})
16281628

1629+
it('should not swallow errors when bailing out early', () => {
1630+
const store = createStore(stringBuilder)
1631+
let renderCalls = 0
1632+
let mapStateCalls = 0
1633+
1634+
@connect(state => {
1635+
mapStateCalls++
1636+
if (state === 'a') {
1637+
throw new Error('Oops')
1638+
} else {
1639+
return {}
1640+
}
1641+
})
1642+
class Container extends Component {
1643+
render() {
1644+
renderCalls++
1645+
return <Passthrough {...this.props} />
1646+
}
1647+
}
1648+
1649+
TestUtils.renderIntoDocument(
1650+
<ProviderMock store={store}>
1651+
<Container />
1652+
</ProviderMock>
1653+
)
1654+
1655+
expect(renderCalls).toBe(1)
1656+
expect(mapStateCalls).toBe(1)
1657+
expect(
1658+
() => store.dispatch({ type: 'APPEND', body: 'a' })
1659+
).toThrow('Oops')
1660+
})
1661+
16291662
it('should allow providing a factory function to mapStateToProps', () => {
16301663
let updatedCount = 0
16311664
let memoizedReturnCount = 0
@@ -1788,5 +1821,52 @@ describe('React', () => {
17881821
expect(renderCount).toBe(2)
17891822
})
17901823

1824+
it('should allow to clean up child state in parent componentWillUnmount', () => {
1825+
function reducer(state = { data: null }, action) {
1826+
switch (action.type) {
1827+
case 'fetch':
1828+
return { data: { profile: { name: 'April' } } }
1829+
case 'clean':
1830+
return { data: null }
1831+
default:
1832+
return state
1833+
}
1834+
}
1835+
1836+
@connect(null)
1837+
class Parent extends React.Component {
1838+
componentWillMount() {
1839+
this.props.dispatch({ type: 'fetch' })
1840+
}
1841+
1842+
componentWillUnmount() {
1843+
this.props.dispatch({ type: 'clean' })
1844+
}
1845+
1846+
render() {
1847+
return <Child />
1848+
}
1849+
}
1850+
1851+
@connect(state => ({
1852+
profile: state.data.profile
1853+
}))
1854+
class Child extends React.Component {
1855+
render() {
1856+
return null
1857+
}
1858+
}
1859+
1860+
const store = createStore(reducer)
1861+
const div = document.createElement('div')
1862+
ReactDOM.render(
1863+
<ProviderMock store={store}>
1864+
<Parent />
1865+
</ProviderMock>,
1866+
div
1867+
)
1868+
1869+
ReactDOM.unmountComponentAtNode(div)
1870+
})
17911871
})
17921872
})

0 commit comments

Comments
 (0)