From 576f3f23b578119cd515b2f75625298b8dbe418c Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Fri, 9 Mar 2018 17:31:54 -0500 Subject: [PATCH 1/4] Hacked-up version of Connect and Provider to use new context API --- src/components/Provider.js | 41 +++++++++- src/components/connectAdvanced.js | 119 ++++++++++++++++++++++++------ src/components/context.js | 3 + 3 files changed, 137 insertions(+), 26 deletions(-) create mode 100644 src/components/context.js diff --git a/src/components/Provider.js b/src/components/Provider.js index ac77a003e..c30bc3b55 100644 --- a/src/components/Provider.js +++ b/src/components/Provider.js @@ -1,8 +1,10 @@ -import { Component, Children } from 'react' +import React, { Component, Children } from 'react' import PropTypes from 'prop-types' import { storeShape, subscriptionShape } from '../utils/PropTypes' import warning from '../utils/warning' +import {ReactReduxContext} from "./context"; + let didWarnAboutReceivingStore = false function warnAboutReceivingStore() { if (didWarnAboutReceivingStore) { @@ -20,20 +22,48 @@ function warnAboutReceivingStore() { } export function createProvider(storeKey = 'store', subKey) { - const subscriptionKey = subKey || `${storeKey}Subscription` + //const subscriptionKey = subKey || `${storeKey}Subscription` class Provider extends Component { + /* getChildContext() { return { [storeKey]: this[storeKey], [subscriptionKey]: null } } + */ constructor(props, context) { super(props, context) - this[storeKey] = props.store; + //this[storeKey] = props.store; + + const {store} = props; + + if(!store || !store.getState || !store.dispatch) { + throw new Error("Must pass a valid Redux store as a prop to Provider"); + } + + this.state = { + storeState : store.getState(), + dispatch : store.dispatch, + }; + } + + componentDidMount() { + const {store} = this.props; + + this.unsubscribe = store.subscribe( () => { + console.log("Provider subscription running"); + this.setState({storeState : store.getState()}); + }); } render() { - return Children.only(this.props.children) + console.log("Provider re-rendering"); + + return ( + + {Children.only(this.props.children)} + + ); } } @@ -45,14 +75,17 @@ export function createProvider(storeKey = 'store', subKey) { } } + Provider.propTypes = { store: storeShape.isRequired, children: PropTypes.element.isRequired, } + /* Provider.childContextTypes = { [storeKey]: storeShape.isRequired, [subscriptionKey]: subscriptionShape, } + */ return Provider } diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 3fff8ee26..ededb86b3 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -1,19 +1,20 @@ import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' -import { Component, createElement } from 'react' +import React, { Component, createElement } from 'react' import Subscription from '../utils/Subscription' +import {ReactReduxContext} from "./context"; import { storeShape, subscriptionShape } from '../utils/PropTypes' let hotReloadingVersion = 0 const dummyState = {} function noop() {} -function makeSelectorStateful(sourceSelector, store) { +function makeSelectorStateful(sourceSelector) { // wrap the selector in an object that tracks its results between runs. const selector = { - run: function runComponentSelector(props) { + run: function runComponentSelector(props, storeState) { try { - const nextProps = sourceSelector(store.getState(), props) + const nextProps = sourceSelector(storeState, props) if (nextProps !== selector.props || selector.error) { selector.shouldComponentUpdate = true selector.props = nextProps @@ -78,6 +79,7 @@ export default function connectAdvanced( const subscriptionKey = storeKey + 'Subscription' const version = hotReloadingVersion++ + /* const contextTypes = { [storeKey]: storeShape, [subscriptionKey]: subscriptionShape, @@ -85,10 +87,11 @@ export default function connectAdvanced( const childContextTypes = { [subscriptionKey]: subscriptionShape, } + */ return function wrapWithConnect(WrappedComponent) { invariant( - typeof WrappedComponent == 'function', + typeof WrappedComponent === 'function', `You must pass a component to the function returned by ` + `${methodName}. Instead received ${JSON.stringify(WrappedComponent)}` ) @@ -117,22 +120,30 @@ export default function connectAdvanced( super(props, context) this.version = version - this.state = {} + //this.state = {} this.renderCount = 0 - this.store = props[storeKey] || context[storeKey] - this.propsMode = Boolean(props[storeKey]) + //this.store = props[storeKey] || context[storeKey] + //this.propsMode = Boolean(props[storeKey]) + + this.storeState = null; + + this.setWrappedInstance = this.setWrappedInstance.bind(this) + this.renderChild = this.renderChild.bind(this); + /* invariant(this.store, `Could not find "${storeKey}" in either the context or props of ` + `"${displayName}". Either wrap the root component in a , ` + `or explicitly pass "${storeKey}" as a prop to "${displayName}".` ) + */ - this.initSelector() - this.initSubscription() + //this.initSelector() + //this.initSubscription() } + /* getChildContext() { // If this component received store from props, its subscription should be transparent // to any descendants receiving store+subscription from context; it passes along @@ -141,6 +152,7 @@ export default function connectAdvanced( const subscription = this.propsMode ? null : this.subscription return { [subscriptionKey]: subscription || this.context[subscriptionKey] } } + */ componentDidMount() { if (!shouldHandleStateChanges) return @@ -151,24 +163,28 @@ export default function connectAdvanced( // To handle the case where a child component may have triggered a state change by // dispatching an action in its componentWillMount, we have to re-run the select and maybe // re-render. - this.subscription.trySubscribe() - this.selector.run(this.props) + //this.subscription.trySubscribe() + this.selector.run(this.props, this.storeState); if (this.selector.shouldComponentUpdate) this.forceUpdate() } - componentWillReceiveProps(nextProps) { - this.selector.run(nextProps) + + UNSAFE_componentWillReceiveProps(nextProps) { + this.selector.run(nextProps, this.storeState); } + + shouldComponentUpdate() { return this.selector.shouldComponentUpdate } + componentWillUnmount() { - if (this.subscription) this.subscription.tryUnsubscribe() - this.subscription = null + //if (this.subscription) this.subscription.tryUnsubscribe() + //this.subscription = null this.notifyNestedSubs = noop - this.store = null + //this.store = null this.selector.run = noop this.selector.shouldComponentUpdate = false } @@ -185,13 +201,22 @@ export default function connectAdvanced( this.wrappedInstance = ref } + /* initSelector() { const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions) this.selector = makeSelectorStateful(sourceSelector, this.store) this.selector.run(this.props) } + */ + + initSelector(dispatch, storeState) { + const sourceSelector = selectorFactory(dispatch, selectorFactoryOptions) + this.selector = makeSelectorStateful(sourceSelector) + this.selector.run(this.props, storeState); + } initSubscription() { + /* if (!shouldHandleStateChanges) return // parentSub's source should match where store came from: props vs. context. A component @@ -206,6 +231,7 @@ export default function connectAdvanced( // listeners logic is changed to not call listeners that have been unsubscribed in the // middle of the notification loop. this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription) + */ } onStateChange() { @@ -229,12 +255,17 @@ export default function connectAdvanced( this.notifyNestedSubs() } + /* isSubscribed() { return Boolean(this.subscription) && this.subscription.isSubscribed() } + */ addExtraProps(props) { - if (!withRef && !renderCountProp && !(this.propsMode && this.subscription)) return props + //if (!withRef && !renderCountProp && !(this.propsMode && this.subscription)) return props + if (!withRef && !renderCountProp) return props; + + // make a shallow copy so that fields added don't leak to the original selector. // this is especially important for 'ref' since that's a reference back to the component // instance. a singleton memoized selector would then be holding a reference to the @@ -242,10 +273,51 @@ export default function connectAdvanced( const withExtras = { ...props } if (withRef) withExtras.ref = this.setWrappedInstance if (renderCountProp) withExtras[renderCountProp] = this.renderCount++ - if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription + //if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription return withExtras } + renderChild(providerValue) { + const {storeState, dispatch} = providerValue; + + this.storeState = storeState; + + //console.log(`Running renderChild (${displayName})`, storeState, this.props); + + if(this.selector) { + this.selector.run(this.props, storeState); + } + else { + this.initSelector(dispatch, storeState); + } + + + + if (this.selector.error) { + throw this.selector.error + } + else if(this.selector.shouldComponentUpdate) { + console.log(`Re-rendering component (${displayName})`, this.selector.props); + this.selector.shouldComponentUpdate = false; + this.renderedElement = createElement(WrappedComponent, this.addExtraProps(this.selector.props)); + } + else { + //console.log(`Returning existing render result (${displayName})`, this.props) + } + + return this.renderedElement; + } + + render() { + return ( + + {this.renderChild} + + ) + } + + /* + render() { const selector = this.selector selector.shouldComponentUpdate = false @@ -256,14 +328,16 @@ export default function connectAdvanced( return createElement(WrappedComponent, this.addExtraProps(selector.props)) } } + */ } Connect.WrappedComponent = WrappedComponent Connect.displayName = displayName - Connect.childContextTypes = childContextTypes - Connect.contextTypes = contextTypes - Connect.propTypes = contextTypes + //Connect.childContextTypes = childContextTypes + //Connect.contextTypes = contextTypes + //Connect.propTypes = contextTypes + /* if (process.env.NODE_ENV !== 'production') { Connect.prototype.componentWillUpdate = function componentWillUpdate() { // We are hot reloading! @@ -290,6 +364,7 @@ export default function connectAdvanced( } } } + */ return hoistStatics(Connect, WrappedComponent) } diff --git a/src/components/context.js b/src/components/context.js new file mode 100644 index 000000000..ba029da20 --- /dev/null +++ b/src/components/context.js @@ -0,0 +1,3 @@ +import React from "react"; + +export const ReactReduxContext = React.createContext(null); \ No newline at end of file From 074c418b48443923e74a1e3225a8d902ab802a19 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Fri, 9 Mar 2018 17:32:36 -0500 Subject: [PATCH 2/4] Sorta-fix a few tests --- test/components/Provider.spec.js | 8 +++++--- test/components/connect.spec.js | 7 ++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/test/components/Provider.spec.js b/test/components/Provider.spec.js index ffc8ae498..e5d8cd6c7 100644 --- a/test/components/Provider.spec.js +++ b/test/components/Provider.spec.js @@ -54,6 +54,7 @@ describe('React', () => { } }) + /* it('should add the store to the child context', () => { const store = createStore(() => ({})) @@ -87,6 +88,7 @@ describe('React', () => { const child = TestUtils.findRenderedComponentWithType(tree, CustomChild) expect(child.context.customStoreKey).toBe(store) }) + */ it('should warn once when receiving a new store in props', () => { const store1 = createStore((state = 10) => state + 1) @@ -109,13 +111,13 @@ describe('React', () => { const container = TestUtils.renderIntoDocument() const child = TestUtils.findRenderedComponentWithType(container, Child) - expect(child.context.store.getState()).toEqual(11) + //expect(child.context.store.getState()).toEqual(11) let spy = expect.spyOn(console, 'error') container.setState({ store: store2 }) spy.destroy() - expect(child.context.store.getState()).toEqual(11) + //expect(child.context.store.getState()).toEqual(11) expect(spy.calls.length).toBe(1) expect(spy.calls[0].arguments[0]).toBe( ' does not support changing `store` on the fly. ' + @@ -129,7 +131,7 @@ describe('React', () => { container.setState({ store: store3 }) spy.destroy() - expect(child.context.store.getState()).toEqual(11) + //expect(child.context.store.getState()).toEqual(11) expect(spy.calls.length).toBe(0) }) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 91ab71fb7..ec1cf97a1 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types' import ReactDOM from 'react-dom' import TestUtils from 'react-dom/test-utils' import { createStore } from 'redux' -import { connect } from '../../src/index' +import { connect, Provider } from '../../src/index' describe('React', () => { describe('connect', () => { @@ -17,6 +17,7 @@ describe('React', () => { } } + /* class ProviderMock extends Component { getChildContext() { return { store: this.props.store } @@ -29,6 +30,8 @@ describe('React', () => { ProviderMock.childContextTypes = { store: PropTypes.object.isRequired } + */ + const ProviderMock = Provider; class ContextBoundStore { constructor(reducer) { @@ -73,6 +76,7 @@ describe('React', () => { container.forceUpdate() } + /* it('should receive the store in the context', () => { const store = createStore(() => ({})) @@ -92,6 +96,7 @@ describe('React', () => { const container = TestUtils.findRenderedComponentWithType(tree, Container) expect(container.context.store).toBe(store) }) + */ it('should pass state and props to the given component', () => { const store = createStore(() => ({ From e24d98e07f2487e5e028df51cbe6bbf4268adca8 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Fri, 9 Mar 2018 18:01:26 -0500 Subject: [PATCH 3/4] Clean up Provider and connectAdvanced --- src/components/Provider.js | 23 +----- src/components/connectAdvanced.js | 125 +++--------------------------- 2 files changed, 13 insertions(+), 135 deletions(-) diff --git a/src/components/Provider.js b/src/components/Provider.js index c30bc3b55..2fe3e72f8 100644 --- a/src/components/Provider.js +++ b/src/components/Provider.js @@ -1,6 +1,6 @@ import React, { Component, Children } from 'react' import PropTypes from 'prop-types' -import { storeShape, subscriptionShape } from '../utils/PropTypes' +import { storeShape } from '../utils/PropTypes' import warning from '../utils/warning' import {ReactReduxContext} from "./context"; @@ -22,25 +22,14 @@ function warnAboutReceivingStore() { } export function createProvider(storeKey = 'store', subKey) { - //const subscriptionKey = subKey || `${storeKey}Subscription` class Provider extends Component { - /* - getChildContext() { - return { [storeKey]: this[storeKey], [subscriptionKey]: null } - } - */ constructor(props, context) { super(props, context) - //this[storeKey] = props.store; const {store} = props; - if(!store || !store.getState || !store.dispatch) { - throw new Error("Must pass a valid Redux store as a prop to Provider"); - } - this.state = { storeState : store.getState(), dispatch : store.dispatch, @@ -50,15 +39,13 @@ export function createProvider(storeKey = 'store', subKey) { componentDidMount() { const {store} = this.props; + // TODO What about any actions that might have been dispatched between ctor and cDM? this.unsubscribe = store.subscribe( () => { - console.log("Provider subscription running"); this.setState({storeState : store.getState()}); }); } render() { - console.log("Provider re-rendering"); - return ( {Children.only(this.props.children)} @@ -80,12 +67,6 @@ export function createProvider(storeKey = 'store', subKey) { store: storeShape.isRequired, children: PropTypes.element.isRequired, } - /* - Provider.childContextTypes = { - [storeKey]: storeShape.isRequired, - [subscriptionKey]: subscriptionShape, - } - */ return Provider } diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index ededb86b3..9732d7903 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -4,7 +4,7 @@ import React, { Component, createElement } from 'react' import Subscription from '../utils/Subscription' import {ReactReduxContext} from "./context"; -import { storeShape, subscriptionShape } from '../utils/PropTypes' +import { storeShape } from '../utils/PropTypes' let hotReloadingVersion = 0 const dummyState = {} @@ -79,15 +79,6 @@ export default function connectAdvanced( const subscriptionKey = storeKey + 'Subscription' const version = hotReloadingVersion++ - /* - const contextTypes = { - [storeKey]: storeShape, - [subscriptionKey]: subscriptionShape, - } - const childContextTypes = { - [subscriptionKey]: subscriptionShape, - } - */ return function wrapWithConnect(WrappedComponent) { invariant( @@ -116,21 +107,18 @@ export default function connectAdvanced( } class Connect extends Component { - constructor(props, context) { - super(props, context) + constructor(props) { + super(props) this.version = version - //this.state = {} this.renderCount = 0 - //this.store = props[storeKey] || context[storeKey] - //this.propsMode = Boolean(props[storeKey]) - this.storeState = null; this.setWrappedInstance = this.setWrappedInstance.bind(this) this.renderChild = this.renderChild.bind(this); + // TODO How do we express the invariant of needing a Provider when it's used in render()? /* invariant(this.store, `Could not find "${storeKey}" in either the context or props of ` + @@ -138,22 +126,8 @@ export default function connectAdvanced( `or explicitly pass "${storeKey}" as a prop to "${displayName}".` ) */ - - //this.initSelector() - //this.initSubscription() } - /* - getChildContext() { - // If this component received store from props, its subscription should be transparent - // to any descendants receiving store+subscription from context; it passes along - // subscription passed to it. Otherwise, it shadows the parent subscription, which allows - // Connect to control ordering of notifications to flow top-down. - const subscription = this.propsMode ? null : this.subscription - return { [subscriptionKey]: subscription || this.context[subscriptionKey] } - } - */ - componentDidMount() { if (!shouldHandleStateChanges) return @@ -170,21 +144,16 @@ export default function connectAdvanced( UNSAFE_componentWillReceiveProps(nextProps) { + // TODO Do we still want/need to implement sCU / cWRP now? this.selector.run(nextProps, this.storeState); } - - shouldComponentUpdate() { return this.selector.shouldComponentUpdate } componentWillUnmount() { - //if (this.subscription) this.subscription.tryUnsubscribe() - //this.subscription = null - this.notifyNestedSubs = noop - //this.store = null this.selector.run = noop this.selector.shouldComponentUpdate = false } @@ -201,71 +170,15 @@ export default function connectAdvanced( this.wrappedInstance = ref } - /* - initSelector() { - const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions) - this.selector = makeSelectorStateful(sourceSelector, this.store) - this.selector.run(this.props) - } - */ - initSelector(dispatch, storeState) { const sourceSelector = selectorFactory(dispatch, selectorFactoryOptions) this.selector = makeSelectorStateful(sourceSelector) this.selector.run(this.props, storeState); } - initSubscription() { - /* - if (!shouldHandleStateChanges) return - - // parentSub's source should match where store came from: props vs. context. A component - // connected to the store via props shouldn't use subscription from context, or vice versa. - const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey] - this.subscription = new Subscription(this.store, parentSub, this.onStateChange.bind(this)) - - // `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in - // the middle of the notification loop, where `this.subscription` will then be null. An - // extra null check every change can be avoided by copying the method onto `this` and then - // replacing it with a no-op on unmount. This can probably be avoided if Subscription's - // listeners logic is changed to not call listeners that have been unsubscribed in the - // middle of the notification loop. - this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription) - */ - } - - onStateChange() { - this.selector.run(this.props) - - if (!this.selector.shouldComponentUpdate) { - this.notifyNestedSubs() - } else { - this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate - this.setState(dummyState) - } - } - - notifyNestedSubsOnComponentDidUpdate() { - // `componentDidUpdate` is conditionally implemented when `onStateChange` determines it - // needs to notify nested subs. Once called, it unimplements itself until further state - // changes occur. Doing it this way vs having a permanent `componentDidUpdate` that does - // a boolean check every time avoids an extra method call most of the time, resulting - // in some perf boost. - this.componentDidUpdate = undefined - this.notifyNestedSubs() - } - - /* - isSubscribed() { - return Boolean(this.subscription) && this.subscription.isSubscribed() - } - */ - addExtraProps(props) { - //if (!withRef && !renderCountProp && !(this.propsMode && this.subscription)) return props if (!withRef && !renderCountProp) return props; - // make a shallow copy so that fields added don't leak to the original selector. // this is especially important for 'ref' since that's a reference back to the component // instance. a singleton memoized selector would then be holding a reference to the @@ -273,7 +186,7 @@ export default function connectAdvanced( const withExtras = { ...props } if (withRef) withExtras.ref = this.setWrappedInstance if (renderCountProp) withExtras[renderCountProp] = this.renderCount++ - //if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription + return withExtras } @@ -282,8 +195,6 @@ export default function connectAdvanced( this.storeState = storeState; - //console.log(`Running renderChild (${displayName})`, storeState, this.props); - if(this.selector) { this.selector.run(this.props, storeState); } @@ -291,13 +202,13 @@ export default function connectAdvanced( this.initSelector(dispatch, storeState); } - - if (this.selector.error) { + // TODO This will unmount the whole tree now that we're throwing in render. Good idea? + // TODO Related: https://github.com/reactjs/react-redux/issues/802 throw this.selector.error } else if(this.selector.shouldComponentUpdate) { - console.log(`Re-rendering component (${displayName})`, this.selector.props); + //console.log(`Re-rendering component (${displayName})`, this.selector.props); this.selector.shouldComponentUpdate = false; this.renderedElement = createElement(WrappedComponent, this.addExtraProps(this.selector.props)); } @@ -315,28 +226,14 @@ export default function connectAdvanced( ) } - - /* - - render() { - const selector = this.selector - selector.shouldComponentUpdate = false - - if (selector.error) { - throw selector.error - } else { - return createElement(WrappedComponent, this.addExtraProps(selector.props)) - } - } - */ } Connect.WrappedComponent = WrappedComponent Connect.displayName = displayName - //Connect.childContextTypes = childContextTypes - //Connect.contextTypes = contextTypes + // TODO We're losing the ability to add a store as a prop. Not sure there's anything we can do about that. //Connect.propTypes = contextTypes + // TODO With connect no longer managing subscriptions, I _think_ is is all unneeded /* if (process.env.NODE_ENV !== 'production') { Connect.prototype.componentWillUpdate = function componentWillUpdate() { From 585c5b4a1fa05139632932c17055afd0d68246e9 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Fri, 9 Mar 2018 18:16:56 -0500 Subject: [PATCH 4/4] Remove now-unused Subscription --- src/components/connectAdvanced.js | 3 -- src/utils/PropTypes.js | 7 --- src/utils/Subscription.js | 87 ------------------------------- 3 files changed, 97 deletions(-) delete mode 100644 src/utils/Subscription.js diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 9732d7903..131faa817 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -2,7 +2,6 @@ import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' import React, { Component, createElement } from 'react' -import Subscription from '../utils/Subscription' import {ReactReduxContext} from "./context"; import { storeShape } from '../utils/PropTypes' @@ -76,7 +75,6 @@ export default function connectAdvanced( ...connectOptions } = {} ) { - const subscriptionKey = storeKey + 'Subscription' const version = hotReloadingVersion++ @@ -137,7 +135,6 @@ export default function connectAdvanced( // To handle the case where a child component may have triggered a state change by // dispatching an action in its componentWillMount, we have to re-run the select and maybe // re-render. - //this.subscription.trySubscribe() this.selector.run(this.props, this.storeState); if (this.selector.shouldComponentUpdate) this.forceUpdate() } diff --git a/src/utils/PropTypes.js b/src/utils/PropTypes.js index 725b02012..3177fa7a3 100644 --- a/src/utils/PropTypes.js +++ b/src/utils/PropTypes.js @@ -1,12 +1,5 @@ import PropTypes from 'prop-types' -export const subscriptionShape = PropTypes.shape({ - trySubscribe: PropTypes.func.isRequired, - tryUnsubscribe: PropTypes.func.isRequired, - notifyNestedSubs: PropTypes.func.isRequired, - isSubscribed: PropTypes.func.isRequired, -}) - export const storeShape = PropTypes.shape({ subscribe: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired, diff --git a/src/utils/Subscription.js b/src/utils/Subscription.js deleted file mode 100644 index db8146799..000000000 --- a/src/utils/Subscription.js +++ /dev/null @@ -1,87 +0,0 @@ -// encapsulates the subscription logic for connecting a component to the redux store, as -// well as nesting subscriptions of descendant components, so that we can ensure the -// ancestor components re-render before descendants - -const CLEARED = null -const nullListeners = { notify() {} } - -function createListenerCollection() { - // the current/next pattern is copied from redux's createStore code. - // TODO: refactor+expose that code to be reusable here? - let current = [] - let next = [] - - return { - clear() { - next = CLEARED - current = CLEARED - }, - - notify() { - const listeners = current = next - for (let i = 0; i < listeners.length; i++) { - listeners[i]() - } - }, - - get() { - return next - }, - - subscribe(listener) { - let isSubscribed = true - if (next === current) next = current.slice() - next.push(listener) - - return function unsubscribe() { - if (!isSubscribed || current === CLEARED) return - isSubscribed = false - - if (next === current) next = current.slice() - next.splice(next.indexOf(listener), 1) - } - } - } -} - -export default class Subscription { - constructor(store, parentSub, onStateChange) { - this.store = store - this.parentSub = parentSub - this.onStateChange = onStateChange - this.unsubscribe = null - this.listeners = nullListeners - } - - addNestedSub(listener) { - this.trySubscribe() - return this.listeners.subscribe(listener) - } - - notifyNestedSubs() { - this.listeners.notify() - } - - isSubscribed() { - return Boolean(this.unsubscribe) - } - - trySubscribe() { - if (!this.unsubscribe) { - this.unsubscribe = this.parentSub - ? this.parentSub.addNestedSub(this.onStateChange) - : this.store.subscribe(this.onStateChange) - - this.listeners = createListenerCollection() - } - } - - tryUnsubscribe() { - if (this.unsubscribe) { - this.unsubscribe() - this.unsubscribe = null - this.listeners.clear() - this.listeners = nullListeners - } - } -}