Repository: reduxjs/react-redux (ts)

Issue:
Provider uses previous store for one additional render when store changes
**Do you want to request a _feature_ or report a _bug_?**

Bug

**What is the current behavior?**

If a the `store` passed to a `Provider` changes the `store` in `ReactReduxContext` does not update until the next render.  This is causing me an issue the `children` are changing, causing the to crash when trying to `mapStateToProps` in that additional render.

[Sandbox.](https://codesandbox.io/s/thirsty-cherry-f49el)

Note: this is a contrived example to to show how this occurs, but I have an real world case using React Router V5 that has the same issue.

Also Note: my use case is not to have a distinct store per page, but actually using `redux-subspace` to create a virtual sub-store per page.  Again, I didn't bother with this detail for the example as it occurs with real stores too.

**What is the expected behavior?**

I would expect that if `Provider` permits me to change the store, that a `connect`ed component below it would not crash when rendering.  I believe the root of the issue is the fact that [the store is not updated in the `Providers`'s state until `componentDidUpdate`](https://github.com/reduxjs/react-redux/blob/master/src/components/Provider.js#L38-L45), but the render the would result in this update already has the new `this.props.children` value.

I do concede that this is a pretty niche use case, and may not be the highest priority to fix (assuming you agree that it is an issue at all).

**Which versions of React, ReactDOM/React Native, Redux, and React Redux are you using?  Which browser and OS are affected by this issue? Did this work in previous versions of React Redux?**

```json
"react": "16.8.6",
"react-dom": "16.8.6",
"redux": "4.0.4"
"react-redux": "7.1.0",
```

Pull request:
Convert Provider into function component with hooks
Fixes issue where the `store` and `children` values were updated in different render passes which could result in `mapState` errors if the new children were not compatible with the old store.

The change to a function component meant `Provider` could take advantage of `useEffect` to "watch" the `store` prop and unsubscribe to the old store and subscribe to the new store.

Fixes #1370

Patch:
diff --git a/src/components/Provider.js b/src/components/Provider.js
index 6b3e46b..3c5ec0e 100644
--- a/src/components/Provider.js
+++ b/src/components/Provider.js
@@ -1,62 +1,36 @@
-import React, { Component } from 'react'
+import React, { useMemo, useEffect } from 'react'
 import PropTypes from 'prop-types'
 import { ReactReduxContext } from './Context'
 import Subscription from '../utils/Subscription'
 
-class Provider extends Component {
-  constructor(props) {
-    super(props)
-
-    const { store } = props
-
-    this.notifySubscribers = this.notifySubscribers.bind(this)
+function Provider({ store, context, children }) {
+  const contextValue = useMemo(() => {
     const subscription = new Subscription(store)
-    subscription.onStateChange = this.notifySubscribers
-
-    this.state = {
+    subscription.onStateChange = subscription.notifyNestedSubs
+    return {
       store,
       subscription
     }
+  }, [store])
 
-    this.previousState = store.getState()
-  }
+  const previousState = useMemo(() => store.getState(), [store])
 
-  componentDidMount() {
-    this.state.subscription.trySubscribe()
+  useEffect(() => {
+    const { subscription } = contextValue
+    subscription.trySubscribe()
 
-    if (this.previousState !== this.props.store.getState()) {
-      this.state.subscription.notifyNestedSubs()
+    if (previousState !== store.getState()) {
+      subscription.notifyNestedSubs()
     }
-  }
-
-  componentWillUnmount() {
-    if (this.unsubscribe) this.unsubscribe()
-
-    this.state.subscription.tryUnsubscribe()
-  }
-
-  componentDidUpdate(prevProps) {
-    if (this.props.store !== prevProps.store) {
-      this.state.subscription.tryUnsubscribe()
-      const subscription = new Subscription(this.props.store)
-      subscription.onStateChange = this.notifySubscribers
-      this.setState({ store: this.props.store, subscription })
+    return () => {
+      subscription.tryUnsubscribe()
+      subscription.onStateChange = null
     }
-  }
-
-  notifySubscribers() {
-    this.state.subscription.notifyNestedSubs()
-  }
+  }, [contextValue, previousState])
 
-  render() {
-    const Context = this.props.context || ReactReduxContext
+  const Context = context || ReactReduxContext
 
-    return (
-      <Context.Provider value={this.state}>
-        {this.props.children}
-      </Context.Provider>
-    )
-  }
+  return <Context.Provider value={contextValue}>{children}</Context.Provider>
 }
 
 Provider.propTypes = {
diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js
index ccc929a..e3e6f1d 100644
--- a/src/components/connectAdvanced.js
+++ b/src/components/connectAdvanced.js
@@ -368,6 +368,7 @@ export default function connectAdvanced(
         const unsubscribeWrapper = () => {
           didUnsubscribe = true
           subscription.tryUnsubscribe()
+          subscription.onStateChange = null
 
           if (lastThrownError) {
             // It's possible that we caught an error due to a bad mapState function, but the


Test patch:
diff --git a/test/components/Provider.spec.js b/test/components/Provider.spec.js
index 1b95ce3..c8351f6 100644
--- a/test/components/Provider.spec.js
+++ b/test/components/Provider.spec.js
@@ -312,5 +312,43 @@ describe('React', () => {
       ReactDOM.unmountComponentAtNode(div)
       expect(spy).toHaveBeenCalledTimes(1)
     })
+
+    it('should handle store and children change in a the same render', () => {
+      const reducerA = (state = { nestedA: { value: 'expectedA' } }) => state
+      const reducerB = (state = { nestedB: { value: 'expectedB' } }) => state
+
+      const storeA = createStore(reducerA)
+      const storeB = createStore(reducerB)
+
+      @connect(state => ({ value: state.nestedA.value }))
+      class ComponentA extends Component {
+        render() {
+          return <div data-testid="value">{this.props.value}</div>
+        }
+      }
+
+      @connect(state => ({ value: state.nestedB.value }))
+      class ComponentB extends Component {
+        render() {
+          return <div data-testid="value">{this.props.value}</div>
+        }
+      }
+
+      const { getByTestId, rerender } = rtl.render(
+        <Provider store={storeA}>
+          <ComponentA />
+        </Provider>
+      )
+
+      expect(getByTestId('value')).toHaveTextContent('expectedA')
+
+      rerender(
+        <Provider store={storeB}>
+          <ComponentB />
+        </Provider>
+      )
+
+      expect(getByTestId('value')).toHaveTextContent('expectedB')
+    })
   })
 })