From 5ab306b2cceac3b1f3aa3befe251345e8b4da0b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20M=20Sobrepere=20Profit=C3=B3s?= Date: Sun, 5 May 2019 22:04:45 +0200 Subject: [PATCH 1/3] Avoid unnecessary selector evaluations --- src/hooks/useSelector.js | 13 +++++--- test/hooks/useSelector.spec.js | 58 +++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/hooks/useSelector.js b/src/hooks/useSelector.js index 8e40cfa10..ba6befdfe 100644 --- a/src/hooks/useSelector.js +++ b/src/hooks/useSelector.js @@ -50,12 +50,17 @@ export function useSelector(selector) { ]) const latestSubscriptionCallbackError = useRef() - const latestSelector = useRef(selector) + const latestSelector = useRef() + const latestSelectedState = useRef() - let selectedState = undefined + let selectedState try { - selectedState = selector(store.getState()) + selectedState = + selector !== latestSelector.current || + latestSubscriptionCallbackError.current + ? selector(store.getState()) + : latestSelectedState.current } catch (err) { let errorMessage = `An error occured while selecting the store state: ${ err.message @@ -70,8 +75,6 @@ export function useSelector(selector) { throw new Error(errorMessage) } - const latestSelectedState = useRef(selectedState) - useIsomorphicLayoutEffect(() => { latestSelector.current = selector latestSelectedState.current = selectedState diff --git a/test/hooks/useSelector.spec.js b/test/hooks/useSelector.spec.js index c37c5b6e6..22d1e6d9b 100644 --- a/test/hooks/useSelector.spec.js +++ b/test/hooks/useSelector.spec.js @@ -1,6 +1,6 @@ /*eslint-disable react/prop-types*/ -import React from 'react' +import React, { useCallback, useReducer } from 'react' import { createStore } from 'redux' import { renderHook, act } from 'react-hooks-testing-library' import * as rtl from 'react-testing-library' @@ -47,6 +47,29 @@ describe('React', () => { }) describe('lifeycle interactions', () => { + it('always uses the latest state', () => { + store = createStore(c => c + 1, -1) + + const Comp = () => { + const selector = useCallback(c => c + 1, []) + const value = useSelector(selector) + renderedItems.push(value) + return
+ } + + rtl.render( + + + + ) + + expect(renderedItems).toEqual([1]) + + store.dispatch({ type: '' }) + + expect(renderedItems).toEqual([1, 2]) + }) + it('subscribes to the store synchronously', () => { let rootSubscription @@ -156,6 +179,39 @@ describe('React', () => { }) }) + it('uses the latest selector', () => { + let selectorId = 0 + let forceRender + + const Comp = () => { + const [, f] = useReducer(c => c + 1, 0) + forceRender = f + const renderedSelectorId = selectorId++ + const value = useSelector(() => renderedSelectorId) + renderedItems.push(value) + return
+ } + + rtl.render( + + + + ) + + expect(renderedItems).toEqual([0]) + + rtl.act(forceRender) + expect(renderedItems).toEqual([0, 1]) + + rtl.act(() => { + store.dispatch({ type: '' }) + }) + expect(renderedItems).toEqual([0, 1]) + + rtl.act(forceRender) + expect(renderedItems).toEqual([0, 1, 2]) + }) + describe('edge cases', () => { it('ignores transient errors in selector (e.g. due to stale props)', () => { const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) From d381e2698228cab403bbd844776c2e937288cacc Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 19 May 2019 19:44:36 -0400 Subject: [PATCH 2/3] Clean up state assignment logic --- src/hooks/useSelector.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/hooks/useSelector.js b/src/hooks/useSelector.js index ba6befdfe..89abd834d 100644 --- a/src/hooks/useSelector.js +++ b/src/hooks/useSelector.js @@ -56,11 +56,14 @@ export function useSelector(selector) { let selectedState try { - selectedState = + if ( selector !== latestSelector.current || latestSubscriptionCallbackError.current - ? selector(store.getState()) - : latestSelectedState.current + ) { + selectedState = selector(store.getState()) + } else { + selectedState = latestSelectedState.current + } } catch (err) { let errorMessage = `An error occured while selecting the store state: ${ err.message From 4dc56d5b7b6e82b305b49929af4433832ad76c22 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 19 May 2019 19:44:48 -0400 Subject: [PATCH 3/3] Add missing shallowEqual export --- src/alternate-renderers.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/alternate-renderers.js b/src/alternate-renderers.js index c3a04a254..0625e8cb2 100644 --- a/src/alternate-renderers.js +++ b/src/alternate-renderers.js @@ -8,6 +8,7 @@ import { useSelector } from './hooks/useSelector' import { useStore } from './hooks/useStore' import { getBatch } from './utils/batch' +import shallowEqual from './utils/shallowEqual' // For other renderers besides ReactDOM and React Native, use the default noop batch function const batch = getBatch() @@ -20,5 +21,6 @@ export { batch, useDispatch, useSelector, - useStore + useStore, + shallowEqual }