diff --git a/src/components/Provider.js b/src/components/Provider.js index 51f6f7670..303f2e822 100644 --- a/src/components/Provider.js +++ b/src/components/Provider.js @@ -1,5 +1,6 @@ import { Component, PropTypes, Children } from 'react' import storeShape from '../utils/storeShape' +import batchedUpdates from '../utils/batchedUpdates' let didWarnAboutReceivingStore = false function warnAboutReceivingStore() { @@ -21,6 +22,65 @@ function warnAboutReceivingStore() { /* eslint-disable no-console */ } +function batchListenerCalls(store) { + let currentListeners = [] + let nextListeners = currentListeners + + const ensureCanMutateNextListeners = () => { + if (nextListeners === currentListeners) { + nextListeners = currentListeners.slice() + } + } + + let notifyListeners = () => { + const listeners = currentListeners = nextListeners + for (let i = 0; i < listeners.length; i++) { + listeners[i]() + } + } + + let batchListener = () => { + batchedUpdates(notifyListeners) + } + + let unsubscribeBatchListener + + return { + ...store, + subscribe(listener) { + if (typeof listener !== 'function') { + throw new Error('Expected listener to be a function.') + } + + let isSubscribed = true + + ensureCanMutateNextListeners() + nextListeners.push(listener) + + if (!unsubscribeBatchListener) { + unsubscribeBatchListener = store.subscribe(batchListener) + } + + return () => { + if (!isSubscribed) { + return + } + + isSubscribed = false + + ensureCanMutateNextListeners() + const index = nextListeners.indexOf(listener) + nextListeners.splice(index, 1) + + if (!nextListeners.length && unsubscribeBatchListener) { + unsubscribeBatchListener() + unsubscribeBatchListener = null + } + } + } + } +} + export default class Provider extends Component { getChildContext() { return { store: this.store } @@ -28,7 +88,7 @@ export default class Provider extends Component { constructor(props, context) { super(props, context) - this.store = props.store + this.store = batchListenerCalls(props.store) } render() { diff --git a/src/utils/batchedUpdates.js b/src/utils/batchedUpdates.js new file mode 100644 index 000000000..bff4cdaab --- /dev/null +++ b/src/utils/batchedUpdates.js @@ -0,0 +1 @@ +export { unstable_batchedUpdates as default } from 'react-dom' diff --git a/src/utils/batchedUpdates.native.js b/src/utils/batchedUpdates.native.js new file mode 100644 index 000000000..b6b93efe1 --- /dev/null +++ b/src/utils/batchedUpdates.native.js @@ -0,0 +1 @@ +export { unstable_batchedUpdates as default } from 'react-native' diff --git a/test/components/Provider.spec.js b/test/components/Provider.spec.js index 8aa902ffa..e796fed57 100644 --- a/test/components/Provider.spec.js +++ b/test/components/Provider.spec.js @@ -1,8 +1,9 @@ import expect from 'expect' import React, { PropTypes, Component } from 'react' +import ReactDOM from 'react-dom' import TestUtils from 'react-addons-test-utils' import { createStore } from 'redux' -import { Provider } from '../../src/index' +import { Provider, connect } from '../../src/index' describe('React', () => { describe('Provider', () => { @@ -60,7 +61,10 @@ describe('React', () => { expect(spy.calls.length).toBe(0) const child = TestUtils.findRenderedComponentWithType(tree, Child) - expect(child.context.store).toBe(store) + expect(child.context.store).toExist() + expect(child.context.store.dispatch).toBeA('function') + expect(child.context.store.getState).toBeA('function') + expect(child.context.store.subscribe).toBeA('function') }) it('should warn once when receiving a new store in props', () => { @@ -107,5 +111,70 @@ describe('React', () => { expect(child.context.store.getState()).toEqual(11) expect(spy.calls.length).toBe(0) }) + + it('should pass state consistently to mapState', () => { + function stringBuilder(prev = '', action) { + return action.type === 'APPEND' + ? prev + action.body + : prev + } + + const store = createStore(stringBuilder) + + store.dispatch({ type: 'APPEND', body: 'a' }) + let childMapStateInvokes = 0 + + @connect(state => ({ state }), null, null, { withRef: true }) + class Container extends Component { + emitChange() { + store.dispatch({ type: 'APPEND', body: 'b' }) + } + + render() { + return ( +