From ea9538274c00efdc46b4b8e0bc39a939e43ea267 Mon Sep 17 00:00:00 2001 From: Julian Grinblat Date: Fri, 17 May 2019 02:13:15 +0900 Subject: [PATCH 1/2] Replace shallowEqual with reference equality in useSelector --- src/hooks/useSelector.js | 3 +-- test/hooks/useSelector.spec.js | 12 ++++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/hooks/useSelector.js b/src/hooks/useSelector.js index 8e40cfa10..a32385e4d 100644 --- a/src/hooks/useSelector.js +++ b/src/hooks/useSelector.js @@ -1,7 +1,6 @@ import { useReducer, useRef, useEffect, useMemo, useLayoutEffect } from 'react' import invariant from 'invariant' import { useReduxContext } from './useReduxContext' -import shallowEqual from '../utils/shallowEqual' import Subscription from '../utils/Subscription' // React currently throws a warning when using useLayoutEffect on the server. @@ -83,7 +82,7 @@ export function useSelector(selector) { try { const newSelectedState = latestSelector.current(store.getState()) - if (shallowEqual(newSelectedState, latestSelectedState.current)) { + if (newSelectedState === latestSelectedState.current) { return } diff --git a/test/hooks/useSelector.spec.js b/test/hooks/useSelector.spec.js index c37c5b6e6..38d5079d2 100644 --- a/test/hooks/useSelector.spec.js +++ b/test/hooks/useSelector.spec.js @@ -128,16 +128,12 @@ describe('React', () => { }) describe('performance optimizations and bail-outs', () => { - it('should shallowly compare the selected state to prevent unnecessary updates', () => { - store = createStore( - ({ count, stable } = { count: -1, stable: {} }) => ({ - count: count + 1, - stable - }) - ) + it('should compare the selected state by reference to prevent unnecessary updates', () => { + const state = {} + store = createStore(() => state) const Comp = () => { - const value = useSelector(s => Object.keys(s)) + const value = useSelector(s => s) renderedItems.push(value) return
} From 062d5fc66f7582028afa78c411504616ab7ae066 Mon Sep 17 00:00:00 2001 From: Josep M Sobrepere Date: Sun, 19 May 2019 03:38:31 +0200 Subject: [PATCH 2/2] useSelector: Allow optional compararison function Export shallowEqual function --- package-lock.json | 3 +-- src/hooks/useSelector.js | 7 +++++-- src/index.js | 4 +++- test/hooks/useSelector.spec.js | 35 ++++++++++++++++++++++++++++++++-- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83be93f56..0cbd3105d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5010,8 +5010,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "optional": true + "dev": true }, "is-fullwidth-code-point": { "version": "2.0.0", diff --git a/src/hooks/useSelector.js b/src/hooks/useSelector.js index a32385e4d..8f331f9cd 100644 --- a/src/hooks/useSelector.js +++ b/src/hooks/useSelector.js @@ -14,6 +14,8 @@ import Subscription from '../utils/Subscription' const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect +const refEquality = (a, b) => a === b + /** * A hook to access the redux store's state. This hook takes a selector function * as an argument. The selector is called with the store state. @@ -23,6 +25,7 @@ const useIsomorphicLayoutEffect = * useful if you provide a selector that memoizes values). * * @param {Function} selector the selector function + * @param {Function} equalityFn the function that will be used to determine equality * * @returns {any} the selected state * @@ -37,7 +40,7 @@ const useIsomorphicLayoutEffect = * return
{counter}
* } */ -export function useSelector(selector) { +export function useSelector(selector, equalityFn = refEquality) { invariant(selector, `You must pass a selector to useSelectors`) const { store, subscription: contextSub } = useReduxContext() @@ -82,7 +85,7 @@ export function useSelector(selector) { try { const newSelectedState = latestSelector.current(store.getState()) - if (newSelectedState === latestSelectedState.current) { + if (equalityFn(newSelectedState, latestSelectedState.current)) { return } diff --git a/src/index.js b/src/index.js index 654d5e7fd..8817a27aa 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ import { useStore } from './hooks/useStore' import { setBatch } from './utils/batch' import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates' +import shallowEqual from './utils/shallowEqual' setBatch(batch) @@ -20,5 +21,6 @@ export { batch, useDispatch, useSelector, - useStore + useStore, + shallowEqual } diff --git a/test/hooks/useSelector.spec.js b/test/hooks/useSelector.spec.js index 38d5079d2..8f9409234 100644 --- a/test/hooks/useSelector.spec.js +++ b/test/hooks/useSelector.spec.js @@ -4,7 +4,11 @@ import React from 'react' import { createStore } from 'redux' import { renderHook, act } from 'react-hooks-testing-library' import * as rtl from 'react-testing-library' -import { Provider as ProviderMock, useSelector } from '../../src/index.js' +import { + Provider as ProviderMock, + useSelector, + shallowEqual +} from '../../src/index.js' import { useReduxContext } from '../../src/hooks/useReduxContext' describe('React', () => { @@ -128,7 +132,7 @@ describe('React', () => { }) describe('performance optimizations and bail-outs', () => { - it('should compare the selected state by reference to prevent unnecessary updates', () => { + it('defaults to ref-equality to prevent unnecessary updates', () => { const state = {} store = createStore(() => state) @@ -150,6 +154,33 @@ describe('React', () => { expect(renderedItems.length).toBe(1) }) + + it('allows other equality functions to prevent unnecessary updates', () => { + store = createStore( + ({ count, stable } = { count: -1, stable: {} }) => ({ + count: count + 1, + stable + }) + ) + + const Comp = () => { + const value = useSelector(s => Object.keys(s), shallowEqual) + renderedItems.push(value) + return
+ } + + rtl.render( + + + + ) + + expect(renderedItems.length).toBe(1) + + store.dispatch({ type: '' }) + + expect(renderedItems.length).toBe(1) + }) }) describe('edge cases', () => {