Skip to content

Commit a5e45d9

Browse files
dsgkirkbytimdorr
authored andcommitted
Persist child listeners through hot reload (reduxjs#715)
1 parent 9961062 commit a5e45d9

File tree

3 files changed

+95
-17
lines changed

3 files changed

+95
-17
lines changed

src/components/connectAdvanced.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,22 @@ export default function connectAdvanced(
271271
this.version = version
272272
this.initSelector()
273273

274-
if (this.subscription) this.subscription.tryUnsubscribe()
274+
// If any connected descendants don't hot reload (and resubscribe in the process), their
275+
// listeners will be lost when we unsubscribe. Unfortunately, by copying over all
276+
// listeners, this does mean that the old versions of connected descendants will still be
277+
// notified of state changes; however, their onStateChange function is a no-op so this
278+
// isn't a huge deal.
279+
let oldListeners = [];
280+
281+
if (this.subscription) {
282+
oldListeners = this.subscription.listeners.get()
283+
this.subscription.tryUnsubscribe()
284+
}
275285
this.initSubscription()
276-
if (shouldHandleStateChanges) this.subscription.trySubscribe()
286+
if (shouldHandleStateChanges) {
287+
this.subscription.trySubscribe()
288+
oldListeners.forEach(listener => this.subscription.listeners.subscribe(listener))
289+
}
277290
}
278291
}
279292
}

src/utils/Subscription.js

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ function createListenerCollection() {
2424
}
2525
},
2626

27+
get() {
28+
return next
29+
},
30+
2731
subscribe(listener) {
2832
let isSubscribed = true
2933
if (next === current) next = current.slice()

test/components/connect.spec.js

+76-15
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,19 @@ describe('React', () => {
6060
: prev
6161
}
6262

63+
function imitateHotReloading(TargetClass, SourceClass, container) {
64+
// Crude imitation of hot reloading that does the job
65+
Object.getOwnPropertyNames(SourceClass.prototype).filter(key =>
66+
typeof SourceClass.prototype[key] === 'function'
67+
).forEach(key => {
68+
if (key !== 'render' && key !== 'constructor') {
69+
TargetClass.prototype[key] = SourceClass.prototype[key]
70+
}
71+
})
72+
73+
container.forceUpdate()
74+
}
75+
6376
it('should receive the store in the context', () => {
6477
const store = createStore(() => ({}))
6578

@@ -1375,28 +1388,76 @@ describe('React', () => {
13751388
expect(stub.props.foo).toEqual(undefined)
13761389
expect(stub.props.scooby).toEqual('doo')
13771390

1378-
function imitateHotReloading(TargetClass, SourceClass) {
1379-
// Crude imitation of hot reloading that does the job
1380-
Object.getOwnPropertyNames(SourceClass.prototype).filter(key =>
1381-
typeof SourceClass.prototype[key] === 'function'
1382-
).forEach(key => {
1383-
if (key !== 'render' && key !== 'constructor') {
1384-
TargetClass.prototype[key] = SourceClass.prototype[key]
1385-
}
1386-
})
1387-
1388-
container.forceUpdate()
1389-
}
1390-
1391-
imitateHotReloading(ContainerBefore, ContainerAfter)
1391+
imitateHotReloading(ContainerBefore, ContainerAfter, container)
13921392
expect(stub.props.foo).toEqual('baz')
13931393
expect(stub.props.scooby).toEqual('foo')
13941394

1395-
imitateHotReloading(ContainerBefore, ContainerNext)
1395+
imitateHotReloading(ContainerBefore, ContainerNext, container)
13961396
expect(stub.props.foo).toEqual('bar')
13971397
expect(stub.props.scooby).toEqual('boo')
13981398
})
13991399

1400+
it('should persist listeners through hot update', () => {
1401+
const ACTION_TYPE = "ACTION"
1402+
const store = createStore((state = {actions: 0}, action) => {
1403+
switch (action.type) {
1404+
case ACTION_TYPE: {
1405+
return {
1406+
actions: state.actions + 1
1407+
}
1408+
}
1409+
default:
1410+
return state
1411+
}
1412+
})
1413+
1414+
@connect(
1415+
(state) => ({ actions: state.actions })
1416+
)
1417+
class Child extends Component {
1418+
render() {
1419+
return <Passthrough {...this.props}/>
1420+
}
1421+
}
1422+
1423+
@connect(
1424+
() => ({ scooby: 'doo' })
1425+
)
1426+
class ParentBefore extends Component {
1427+
render() {
1428+
return (
1429+
<Child />
1430+
)
1431+
}
1432+
}
1433+
1434+
@connect(
1435+
() => ({ scooby: 'boo' })
1436+
)
1437+
class ParentAfter extends Component {
1438+
render() {
1439+
return (
1440+
<Child />
1441+
)
1442+
}
1443+
}
1444+
1445+
let container
1446+
TestUtils.renderIntoDocument(
1447+
<ProviderMock store={store}>
1448+
<ParentBefore ref={instance => container = instance}/>
1449+
</ProviderMock>
1450+
)
1451+
1452+
const stub = TestUtils.findRenderedComponentWithType(container, Passthrough)
1453+
1454+
imitateHotReloading(ParentBefore, ParentAfter, container)
1455+
1456+
store.dispatch({type: ACTION_TYPE})
1457+
1458+
expect(stub.props.actions).toEqual(1)
1459+
})
1460+
14001461
it('should set the displayName correctly', () => {
14011462
expect(connect(state => state)(
14021463
class Foo extends Component {

0 commit comments

Comments
 (0)