Skip to content

Commit 46508ae

Browse files
committed
Recalculate state and bindings on hot reload. Fixes #27
1 parent ef53563 commit 46508ae

File tree

2 files changed

+88
-3
lines changed

2 files changed

+88
-3
lines changed

src/components/createConnect.js

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ function areStatePropsEqual(stateProps, nextStateProps) {
3030
return shallowEqual(stateProps, nextStateProps);
3131
}
3232

33+
// Helps track hot reloading.
34+
let nextVersion = 0;
35+
3336
export default function createConnect(React) {
3437
const { Component, PropTypes } = React;
3538
const storeShape = createStoreShape(PropTypes);
@@ -44,6 +47,9 @@ export default function createConnect(React) {
4447
wrapActionCreators(actionCreatorsOrMapDispatchToProps) :
4548
actionCreatorsOrMapDispatchToProps;
4649

50+
// Helps track hot reloading.
51+
const version = nextVersion++;
52+
4753
return DecoratedComponent => class Connect extends Component {
4854
static displayName = `Connect(${getDisplayName(DecoratedComponent)})`;
4955
static DecoratedComponent = DecoratedComponent;
@@ -61,6 +67,7 @@ export default function createConnect(React) {
6167

6268
constructor(props, context) {
6369
super(props, context);
70+
this.version = version;
6471
this.setUnderlyingRef = ::this.setUnderlyingRef;
6572
this.state = {
6673
...this.mapState(props, context),
@@ -72,18 +79,45 @@ export default function createConnect(React) {
7279
return typeof this.unsubscribe === 'function';
7380
}
7481

75-
componentDidMount() {
76-
if (shouldSubscribe) {
82+
trySubscribe() {
83+
if (shouldSubscribe && !this.unsubscribe) {
7784
this.unsubscribe = this.context.store.subscribe(::this.handleChange);
7885
}
7986
}
8087

81-
componentWillUnmount() {
88+
tryUnsubscribe() {
8289
if (this.isSubscribed()) {
8390
this.unsubscribe();
91+
this.unsubscribe = null;
8492
}
8593
}
8694

95+
componentDidMount() {
96+
this.trySubscribe();
97+
}
98+
99+
componentWillUpdate() {
100+
if (process.env.NODE_ENV !== 'production') {
101+
if (this.version === version) {
102+
return;
103+
}
104+
105+
// We are hot reloading!
106+
this.version = version;
107+
108+
// Update the state and bindings.
109+
this.trySubscribe();
110+
this.setState({
111+
...this.mapState(),
112+
...this.mapDispatch()
113+
});
114+
}
115+
}
116+
117+
componentWillUnmount() {
118+
this.tryUnsubscribe();
119+
}
120+
87121
handleChange(props = this.props) {
88122
const nextState = this.mapState(props, this.context);
89123
if (!areStatePropsEqual(this.state.stateProps, nextState.stateProps)) {

test/components/connect.spec.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,57 @@ describe('React', () => {
435435
}).toThrow(/mergeProps/);
436436
});
437437

438+
it('should recalculate the state and rebind the actions on hot update', () => {
439+
const store = createStore(() => {});
440+
441+
@connect(
442+
undefined,
443+
() => ({ scooby: 'doo' })
444+
)
445+
class ContainerBefore extends Component {
446+
render() {
447+
return (
448+
<div {...this.props} />
449+
);
450+
}
451+
}
452+
453+
@connect(
454+
() => ({ foo: 'baz' }),
455+
() => ({ scooby: 'foo' })
456+
)
457+
class ContainerAfter extends Component {
458+
render() {
459+
return (
460+
<div {...this.props} />
461+
);
462+
}
463+
}
464+
465+
let container;
466+
TestUtils.renderIntoDocument(
467+
<Provider store={store}>
468+
{() => <ContainerBefore ref={instance => container = instance} />}
469+
</Provider>
470+
);
471+
const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div');
472+
expect(div.props.foo).toEqual(undefined);
473+
expect(div.props.scooby).toEqual('doo');
474+
475+
// Crude imitation of hot reloading that does the job
476+
Object.keys(ContainerAfter.prototype).filter(key =>
477+
typeof ContainerAfter.prototype[key] === 'function'
478+
).forEach(key => {
479+
if (key !== 'render') {
480+
ContainerBefore.prototype[key] = ContainerAfter.prototype[key];
481+
}
482+
});
483+
484+
container.forceUpdate();
485+
expect(div.props.foo).toEqual('baz');
486+
expect(div.props.scooby).toEqual('foo');
487+
});
488+
438489
it('should set the displayName correctly', () => {
439490
expect(connect(state => state)(
440491
class Foo extends Component {

0 commit comments

Comments
 (0)