Skip to content

Convert Provider into function component with hooks #1377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 18 additions & 44 deletions src/components/Provider.js
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I determined subscription.onStateChange = this.notifySubscribers to actually boiled down to. I'm wondering if subscription couldn't just handle this internally?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use the same Subscription class for the connect components, where this is part of our top-down update mechanic. So, it needs to stay configurable. This is just a quirk of re-use.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is a very old PR, but I'm wondering if you ever tested this in StrictMode? I'm noticing bugs where updates stop happening for us using react-redux 7.2.5, and I believe it's because we set onStateChange here in the useMemo callback, but then we clear it here in the useEffect teardown. With strict mode, the useMemo callback here will run twice, but the result of the second run isn't actually used. So, the result is that children are rendered with the original subscription which is resubscribed but has no onStateChange callback, so it does nothing. I think the fix is to move this line to the useEffect call. I can patch that for us locally, but I'm wondering if you ever encountered this after making this change.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, I see 6acb58d now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MichaelSnowden yeah, we actually fixed that bug in https://github.com/reduxjs/react-redux/releases/tag/v7.2.8 :) latest 7.x release is 7.2.9. (But also, we would recommend upgrading to v8.x if possible if you're still on React 17 or earlier, or v9 if you're on React 18.)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @markerikson ! Do you know if it's possible for this to occur outside of StrictMode with React 18? I don't think we'll be able to upgrade unfortunately, but we can patch this fix. The main thing I'm wondering is if this could explain other times I've heard devs say that the Redux store just stopped updating their components (even outside of strict mode).

I don't understand React concurrent mode fully, but it seems like it might be possible there for something simillar to happen there where the subscription useMemo state is reused, but this effect runs twice (because the component remounted but kept its state somehow). Do you know if that's possible or if this is a bug we could have only seen in strict mode.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strict Mode would be the only real reason I can think of that this would be an issue off the top of my head.

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()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not see where this.unsubscribe was actually being set. I think this could have been safely removed, but the new implementation does not allow for this in any way, so if that is an issue I'll have to go back to the drawing board.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think this line is a leftover artifact from pre-7.0 that I missed removing.


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 = {
Expand Down
38 changes: 38 additions & 0 deletions test/components/Provider.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did verify that this did actually fail on the old code before making my change.

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')
})
})
})