diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43de1eb..ec74446 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,8 @@
# Change Log
## [Unreleased]
+### Changed
+- useSelector avoids stale props without try-catch mitigation
## [4.4.0] - 2019-10-17
### Added
diff --git a/__tests__/03_stale_props.js b/__tests__/03_stale_props.js
new file mode 100644
index 0000000..4448888
--- /dev/null
+++ b/__tests__/03_stale_props.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import { createStore } from 'redux';
+
+import { render, cleanup, act } from '@testing-library/react';
+
+import { Provider, useSelector } from '../src/index';
+
+describe('stale props spec', () => {
+ afterEach(cleanup);
+
+ it('ignores transient errors in selector (e.g. due to stale props)', () => {
+ const Parent = () => {
+ const count = useSelector((state) => state.count);
+ return ;
+ };
+
+ const Child = ({ parentCount }) => {
+ const selector = (state) => {
+ if (state.count !== parentCount) {
+ throw new Error();
+ }
+ return state.count + parentCount;
+ };
+ const result = useSelector(selector);
+ return
{result}
;
+ };
+
+ const store = createStore((state = { count: -1 }) => ({ count: state.count + 1 }));
+
+ const App = () => (
+
+
+
+ );
+
+ render();
+ act(() => {
+ expect(() => store.dispatch({ type: '' })).not.toThrowError();
+ });
+ });
+
+ it('ensures consistency of state and props in selector', () => {
+ let selectorSawInconsistencies = false;
+
+ const Parent = () => {
+ const count = useSelector((state) => state.count);
+ return ;
+ };
+
+ const Child = ({ parentCount }) => {
+ const selector = (state) => {
+ selectorSawInconsistencies = selectorSawInconsistencies || (state.count !== parentCount);
+ return state.count + parentCount;
+ };
+ const result = useSelector(selector);
+ return {result}
;
+ };
+
+ const store = createStore((state = { count: -1 }) => ({ count: state.count + 1 }));
+
+ const App = () => (
+
+
+
+ );
+
+ render();
+ act(() => {
+ store.dispatch({ type: '' });
+ });
+ expect(selectorSawInconsistencies).toBe(false);
+ });
+});
diff --git a/src/useSelector.js b/src/useSelector.js
index 9ea6868..3410d83 100644
--- a/src/useSelector.js
+++ b/src/useSelector.js
@@ -1,14 +1,7 @@
-import {
- useContext,
- useEffect,
- useRef,
- useReducer,
-} from 'react';
+import { useContext, useEffect, useReducer } from 'react';
import { defaultContext } from './Provider';
-import { useIsomorphicLayoutEffect } from './utils';
-
const isFunction = (f) => typeof f === 'function';
const defaultEqualityFn = (a, b) => a === b;
@@ -17,32 +10,14 @@ export const useSelector = (selector, eqlFn, opts) => {
equalityFn = isFunction(eqlFn) ? eqlFn : defaultEqualityFn,
customContext = defaultContext,
} = opts || (!isFunction(eqlFn) && eqlFn) || {};
- const [, forceUpdate] = useReducer((c) => c + 1, 0);
const { state, subscribe } = useContext(customContext);
- const selected = selector(state);
- const ref = useRef(null);
- useIsomorphicLayoutEffect(() => {
- ref.current = {
- equalityFn,
- selector,
- state,
- selected,
- };
- });
+ const [selected, updateSelected] = useReducer((prevSelected) => {
+ const nextSelected = selector(state);
+ if (equalityFn(prevSelected, nextSelected)) return prevSelected;
+ return nextSelected;
+ }, state, selector);
useEffect(() => {
- const callback = (nextState) => {
- try {
- if (ref.current.state === nextState
- || ref.current.equalityFn(ref.current.selected, ref.current.selector(nextState))) {
- // not changed
- return;
- }
- } catch (e) {
- // ignored (stale props or some other reason)
- }
- forceUpdate();
- };
- const unsubscribe = subscribe(callback);
+ const unsubscribe = subscribe(updateSelected);
return unsubscribe;
}, [subscribe]);
return selected;