diff --git a/src/components/Provider.js b/src/components/Provider.js
index ac77a003e..2fe3e72f8 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 { storeShape } from '../utils/PropTypes'
import warning from '../utils/warning'
+import {ReactReduxContext} from "./context";
+
let didWarnAboutReceivingStore = false
function warnAboutReceivingStore() {
if (didWarnAboutReceivingStore) {
@@ -20,20 +22,35 @@ 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;
+
+ this.state = {
+ storeState : store.getState(),
+ dispatch : store.dispatch,
+ };
+ }
+
+ componentDidMount() {
+ const {store} = this.props;
+
+ // TODO What about any actions that might have been dispatched between ctor and cDM?
+ this.unsubscribe = store.subscribe( () => {
+ this.setState({storeState : store.getState()});
+ });
}
render() {
- return Children.only(this.props.children)
+ return (
+
+ {Children.only(this.props.children)}
+
+ );
}
}
@@ -45,14 +62,11 @@ 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..131faa817 100644
--- a/src/components/connectAdvanced.js
+++ b/src/components/connectAdvanced.js
@@ -1,19 +1,19 @@
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 { storeShape, subscriptionShape } from '../utils/PropTypes'
+import {ReactReduxContext} from "./context";
+import { storeShape } 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
@@ -75,20 +75,12 @@ export default function connectAdvanced(
...connectOptions
} = {}
) {
- const subscriptionKey = storeKey + 'Subscription'
const version = hotReloadingVersion++
- const contextTypes = {
- [storeKey]: storeShape,
- [subscriptionKey]: subscriptionShape,
- }
- 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)}`
)
@@ -113,33 +105,25 @@ 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 ` +
`"${displayName}". Either wrap the root component in a , ` +
`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() {
@@ -151,24 +135,22 @@ 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.selector.run(this.props, this.storeState);
if (this.selector.shouldComponentUpdate) this.forceUpdate()
}
- componentWillReceiveProps(nextProps) {
- this.selector.run(nextProps)
+
+ 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
}
@@ -185,56 +167,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)
- }
-
- 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()
+ initSelector(dispatch, storeState) {
+ const sourceSelector = selectorFactory(dispatch, selectorFactoryOptions)
+ this.selector = makeSelectorStateful(sourceSelector)
+ this.selector.run(this.props, storeState);
}
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
@@ -242,28 +183,55 @@ 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
}
- render() {
- const selector = this.selector
- selector.shouldComponentUpdate = false
+ renderChild(providerValue) {
+ const {storeState, dispatch} = providerValue;
- if (selector.error) {
- throw selector.error
- } else {
- return createElement(WrappedComponent, this.addExtraProps(selector.props))
- }
+ this.storeState = storeState;
+
+ if(this.selector) {
+ this.selector.run(this.props, storeState);
+ }
+ else {
+ 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);
+ 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}
+
+ )
}
}
Connect.WrappedComponent = WrappedComponent
Connect.displayName = displayName
- Connect.childContextTypes = childContextTypes
- Connect.contextTypes = contextTypes
- Connect.propTypes = 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() {
// We are hot reloading!
@@ -290,6 +258,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
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
- }
- }
-}
diff --git a/test/components/Provider.spec.js b/test/components/Provider.spec.js
index 449cdf0fb..b251595b2 100644
--- a/test/components/Provider.spec.js
+++ b/test/components/Provider.spec.js
@@ -56,6 +56,7 @@ describe('React', () => {
}
})
+ /*
it('should add the store to the child context', () => {
const store = createStore(() => ({}))
@@ -89,6 +90,7 @@ describe('React', () => {
const child = testRenderer.root.findByType(CustomChild).instance
expect(child.context.customStoreKey).toBe(store)
})
+ */
it('should warn once when receiving a new store in props', () => {
const store1 = createStore((state = 10) => state + 1)
@@ -111,13 +113,13 @@ describe('React', () => {
const testRenderer = TestRenderer.create()
const child = testRenderer.root.findByType(Child).instance
- expect(child.context.store.getState()).toEqual(11)
+ //expect(child.context.store.getState()).toEqual(11)
let spy = jest.spyOn(console, 'error').mockImplementation(() => {})
testRenderer.root.instance.setState({ store: store2 })
spy.mockRestore()
- expect(child.context.store.getState()).toEqual(11)
+ //expect(child.context.store.getState()).toEqual(11)
expect(spy).toHaveBeenCalledTimes(1)
expect(spy.mock.calls[0][0]).toBe(
' does not support changing `store` on the fly. ' +
@@ -131,7 +133,7 @@ describe('React', () => {
testRenderer.root.instance.setState({ store: store3 })
spy.mockRestore()
- expect(child.context.store.getState()).toEqual(11)
+ //expect(child.context.store.getState()).toEqual(11)
expect(spy).toHaveBeenCalledTimes(0)
})
diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js
index 2d3e975ce..e50e1a4a2 100644
--- a/test/components/connect.spec.js
+++ b/test/components/connect.spec.js
@@ -6,7 +6,7 @@ import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import TestRenderer from 'react-test-renderer'
import { createStore } from 'redux'
-import { connect } from '../../src/index'
+import { connect, Provider } from '../../src/index'
describe('React', () => {
describe('connect', () => {
@@ -16,6 +16,7 @@ describe('React', () => {
}
}
+ /*
class ProviderMock extends Component {
getChildContext() {
return { store: this.props.store }
@@ -28,6 +29,8 @@ describe('React', () => {
ProviderMock.childContextTypes = {
store: PropTypes.object.isRequired
}
+ */
+ const ProviderMock = Provider;
class ContextBoundStore {
constructor(reducer) {
@@ -72,6 +75,7 @@ describe('React', () => {
container.forceUpdate()
}
+ /*
it('should receive the store in the context', () => {
const store = createStore(() => ({}))
@@ -91,6 +95,7 @@ describe('React', () => {
const container = testRenderer.root.findByType(Container)
expect(container.instance.context.store).toBe(store)
})
+ */
it('should pass state and props to the given component', () => {
const store = createStore(() => ({