Skip to content

Commit

Permalink
Recalculate state and bindings on hot reload. Fixes #27
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon committed Aug 8, 2015
1 parent ef53563 commit 46508ae
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 3 deletions.
40 changes: 37 additions & 3 deletions src/components/createConnect.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ function areStatePropsEqual(stateProps, nextStateProps) {
return shallowEqual(stateProps, nextStateProps);
}

// Helps track hot reloading.
let nextVersion = 0;

export default function createConnect(React) {
const { Component, PropTypes } = React;
const storeShape = createStoreShape(PropTypes);
Expand All @@ -44,6 +47,9 @@ export default function createConnect(React) {
wrapActionCreators(actionCreatorsOrMapDispatchToProps) :
actionCreatorsOrMapDispatchToProps;

// Helps track hot reloading.
const version = nextVersion++;

return DecoratedComponent => class Connect extends Component {
static displayName = `Connect(${getDisplayName(DecoratedComponent)})`;
static DecoratedComponent = DecoratedComponent;
Expand All @@ -61,6 +67,7 @@ export default function createConnect(React) {

constructor(props, context) {
super(props, context);
this.version = version;
this.setUnderlyingRef = ::this.setUnderlyingRef;
this.state = {
...this.mapState(props, context),
Expand All @@ -72,18 +79,45 @@ export default function createConnect(React) {
return typeof this.unsubscribe === 'function';
}

componentDidMount() {
if (shouldSubscribe) {
trySubscribe() {
if (shouldSubscribe && !this.unsubscribe) {
this.unsubscribe = this.context.store.subscribe(::this.handleChange);
}
}

componentWillUnmount() {
tryUnsubscribe() {
if (this.isSubscribed()) {
this.unsubscribe();
this.unsubscribe = null;
}
}

componentDidMount() {
this.trySubscribe();
}

componentWillUpdate() {
if (process.env.NODE_ENV !== 'production') {
if (this.version === version) {
return;
}

// We are hot reloading!
this.version = version;

// Update the state and bindings.
this.trySubscribe();
this.setState({
...this.mapState(),
...this.mapDispatch()
});
}
}

componentWillUnmount() {
this.tryUnsubscribe();
}

handleChange(props = this.props) {
const nextState = this.mapState(props, this.context);
if (!areStatePropsEqual(this.state.stateProps, nextState.stateProps)) {
Expand Down
51 changes: 51 additions & 0 deletions test/components/connect.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,57 @@ describe('React', () => {
}).toThrow(/mergeProps/);
});

it('should recalculate the state and rebind the actions on hot update', () => {
const store = createStore(() => {});

@connect(
undefined,
() => ({ scooby: 'doo' })
)
class ContainerBefore extends Component {
render() {
return (
<div {...this.props} />
);
}
}

@connect(
() => ({ foo: 'baz' }),
() => ({ scooby: 'foo' })
)
class ContainerAfter extends Component {
render() {
return (
<div {...this.props} />
);
}
}

let container;
TestUtils.renderIntoDocument(
<Provider store={store}>
{() => <ContainerBefore ref={instance => container = instance} />}
</Provider>
);
const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div');
expect(div.props.foo).toEqual(undefined);
expect(div.props.scooby).toEqual('doo');

// Crude imitation of hot reloading that does the job
Object.keys(ContainerAfter.prototype).filter(key =>
typeof ContainerAfter.prototype[key] === 'function'
).forEach(key => {
if (key !== 'render') {
ContainerBefore.prototype[key] = ContainerAfter.prototype[key];
}
});

container.forceUpdate();
expect(div.props.foo).toEqual('baz');
expect(div.props.scooby).toEqual('foo');
});

it('should set the displayName correctly', () => {
expect(connect(state => state)(
class Foo extends Component {
Expand Down

0 comments on commit 46508ae

Please sign in to comment.