Skip to content

Commit 69a6287

Browse files
mpeypermarkerikson
andcommitted
Convert Provider into function component with hooks (reduxjs#1377)
* Convert Provider into function component with hooks Fixes issue where the store and children got updated in different render passes which could result in mapState errors if the new children were not compatible with the old store. Fixes reduxjs#1370 * Remove onStateChange from subscription when unsubscribing Co-Authored-By: Mark Erikson <[email protected]>
1 parent b66b46a commit 69a6287

File tree

2 files changed

+56
-44
lines changed

2 files changed

+56
-44
lines changed

src/components/Provider.js

Lines changed: 18 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,36 @@
1-
import React, { Component } from 'react'
1+
import React, { useMemo, useEffect } from 'react'
22
import PropTypes from 'prop-types'
33
import { ReactReduxContext } from './Context'
44
import Subscription from '../utils/Subscription'
55

6-
class Provider extends Component {
7-
constructor(props) {
8-
super(props)
9-
10-
const { store } = props
11-
12-
this.notifySubscribers = this.notifySubscribers.bind(this)
6+
function Provider({ store, context, children }) {
7+
const contextValue = useMemo(() => {
138
const subscription = new Subscription(store)
14-
subscription.onStateChange = this.notifySubscribers
15-
16-
this.state = {
9+
subscription.onStateChange = subscription.notifyNestedSubs
10+
return {
1711
store,
1812
subscription
1913
}
14+
}, [store])
2015

21-
this.previousState = store.getState()
22-
}
16+
const previousState = useMemo(() => store.getState(), [store])
2317

24-
componentDidMount() {
25-
this.state.subscription.trySubscribe()
18+
useEffect(() => {
19+
const { subscription } = contextValue
20+
subscription.trySubscribe()
2621

27-
if (this.previousState !== this.props.store.getState()) {
28-
this.state.subscription.notifyNestedSubs()
22+
if (previousState !== store.getState()) {
23+
subscription.notifyNestedSubs()
2924
}
30-
}
31-
32-
componentWillUnmount() {
33-
if (this.unsubscribe) this.unsubscribe()
34-
35-
this.state.subscription.tryUnsubscribe()
36-
}
37-
38-
componentDidUpdate(prevProps) {
39-
if (this.props.store !== prevProps.store) {
40-
this.state.subscription.tryUnsubscribe()
41-
const subscription = new Subscription(this.props.store)
42-
subscription.onStateChange = this.notifySubscribers
43-
this.setState({ store: this.props.store, subscription })
25+
return () => {
26+
subscription.tryUnsubscribe()
27+
subscription.onStateChange = null
4428
}
45-
}
46-
47-
notifySubscribers() {
48-
this.state.subscription.notifyNestedSubs()
49-
}
29+
}, [contextValue, previousState])
5030

51-
render() {
52-
const Context = this.props.context || ReactReduxContext
31+
const Context = context || ReactReduxContext
5332

54-
return (
55-
<Context.Provider value={this.state}>
56-
{this.props.children}
57-
</Context.Provider>
58-
)
59-
}
33+
return <Context.Provider value={contextValue}>{children}</Context.Provider>
6034
}
6135

6236
Provider.propTypes = {

test/components/Provider.spec.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,5 +312,43 @@ describe('React', () => {
312312
ReactDOM.unmountComponentAtNode(div)
313313
expect(spy).toHaveBeenCalledTimes(1)
314314
})
315+
316+
it('should handle store and children change in a the same render', () => {
317+
const reducerA = (state = { nestedA: { value: 'expectedA' } }) => state
318+
const reducerB = (state = { nestedB: { value: 'expectedB' } }) => state
319+
320+
const storeA = createStore(reducerA)
321+
const storeB = createStore(reducerB)
322+
323+
@connect(state => ({ value: state.nestedA.value }))
324+
class ComponentA extends Component {
325+
render() {
326+
return <div data-testid="value">{this.props.value}</div>
327+
}
328+
}
329+
330+
@connect(state => ({ value: state.nestedB.value }))
331+
class ComponentB extends Component {
332+
render() {
333+
return <div data-testid="value">{this.props.value}</div>
334+
}
335+
}
336+
337+
const { getByTestId, rerender } = rtl.render(
338+
<Provider store={storeA}>
339+
<ComponentA />
340+
</Provider>
341+
)
342+
343+
expect(getByTestId('value')).toHaveTextContent('expectedA')
344+
345+
rerender(
346+
<Provider store={storeB}>
347+
<ComponentB />
348+
</Provider>
349+
)
350+
351+
expect(getByTestId('value')).toHaveTextContent('expectedB')
352+
})
315353
})
316354
})

0 commit comments

Comments
 (0)