diff --git a/src/hooks/useSelector.js b/src/hooks/useSelector.js
index bfb2d70fe..326e41284 100644
--- a/src/hooks/useSelector.js
+++ b/src/hooks/useSelector.js
@@ -1,7 +1,6 @@
-import { useReducer, useRef, useMemo, useContext } from 'react'
+import { useReducer, useEffect, useMemo, useContext, useRef } from 'react'
import { useReduxContext as useDefaultReduxContext } from './useReduxContext'
import Subscription from '../utils/Subscription'
-import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
import { ReactReduxContext } from '../components/Context'
const refEquality = (a, b) => a === b
@@ -12,63 +11,35 @@ function useSelectorWithStoreAndSubscription(
store,
contextSub
) {
- const [, forceRender] = useReducer(s => s + 1, 0)
+ const dispatching = useRef(false)
+ const [state, dispatch] = useReducer(
+ (prevState, storeState) => {
+ if (dispatching.current) {
+ // schedule update
+ return { ...prevState }
+ }
+ const nextSelectedState = selector(storeState)
+ if (equalityFn(nextSelectedState, prevState.selectedState)) {
+ // bail out
+ return prevState
+ }
+ return { selectedState: nextSelectedState }
+ },
+ store.getState(),
+ storeState => ({ selectedState: selector(storeState) })
+ )
const subscription = useMemo(() => new Subscription(store, contextSub), [
store,
contextSub
])
- const latestSubscriptionCallbackError = useRef()
- const latestSelector = useRef()
- const latestSelectedState = useRef()
-
- let selectedState
-
- try {
- if (
- selector !== latestSelector.current ||
- latestSubscriptionCallbackError.current
- ) {
- selectedState = selector(store.getState())
- } else {
- selectedState = latestSelectedState.current
+ useEffect(() => {
+ const checkForUpdates = () => {
+ dispatching.current = true
+ dispatch(store.getState())
+ dispatching.current = false
}
- } catch (err) {
- if (latestSubscriptionCallbackError.current) {
- err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
- }
-
- throw err
- }
-
- useIsomorphicLayoutEffect(() => {
- latestSelector.current = selector
- latestSelectedState.current = selectedState
- latestSubscriptionCallbackError.current = undefined
- })
-
- useIsomorphicLayoutEffect(() => {
- function checkForUpdates() {
- try {
- const newSelectedState = latestSelector.current(store.getState())
-
- if (equalityFn(newSelectedState, latestSelectedState.current)) {
- return
- }
-
- latestSelectedState.current = newSelectedState
- } catch (err) {
- // we ignore all errors here, since when the component
- // is re-rendered, the selectors are called again, and
- // will throw again, if neither props nor store state
- // changed
- latestSubscriptionCallbackError.current = err
- }
-
- forceRender({})
- }
-
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
@@ -77,7 +48,7 @@ function useSelectorWithStoreAndSubscription(
return () => subscription.tryUnsubscribe()
}, [store, subscription])
- return selectedState
+ return state.selectedState
}
/**
diff --git a/test/integration/stale-props.spec.js b/test/integration/stale-props.spec.js
new file mode 100644
index 000000000..464ee01e3
--- /dev/null
+++ b/test/integration/stale-props.spec.js
@@ -0,0 +1,77 @@
+/*eslint-disable react/prop-types*/
+
+import React from 'react'
+import { createStore } from 'redux'
+import { Provider, useSelector } from '../../src/index.js'
+import * as rtl from '@testing-library/react'
+
+describe('React', () => {
+ describe('stale props with useSelector', () => {
+ it('ignores transient errors in selector (e.g. due to stale props)', () => {
+ const Parent = () => {
+ const count = useSelector(count => count)
+ return