Skip to content

Commit 5ab306b

Browse files
committed
Avoid unnecessary selector evaluations
1 parent a787aee commit 5ab306b

File tree

2 files changed

+65
-6
lines changed

2 files changed

+65
-6
lines changed

src/hooks/useSelector.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,17 @@ export function useSelector(selector) {
5050
])
5151

5252
const latestSubscriptionCallbackError = useRef()
53-
const latestSelector = useRef(selector)
53+
const latestSelector = useRef()
54+
const latestSelectedState = useRef()
5455

55-
let selectedState = undefined
56+
let selectedState
5657

5758
try {
58-
selectedState = selector(store.getState())
59+
selectedState =
60+
selector !== latestSelector.current ||
61+
latestSubscriptionCallbackError.current
62+
? selector(store.getState())
63+
: latestSelectedState.current
5964
} catch (err) {
6065
let errorMessage = `An error occured while selecting the store state: ${
6166
err.message
@@ -70,8 +75,6 @@ export function useSelector(selector) {
7075
throw new Error(errorMessage)
7176
}
7277

73-
const latestSelectedState = useRef(selectedState)
74-
7578
useIsomorphicLayoutEffect(() => {
7679
latestSelector.current = selector
7780
latestSelectedState.current = selectedState

test/hooks/useSelector.spec.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*eslint-disable react/prop-types*/
22

3-
import React from 'react'
3+
import React, { useCallback, useReducer } from 'react'
44
import { createStore } from 'redux'
55
import { renderHook, act } from 'react-hooks-testing-library'
66
import * as rtl from 'react-testing-library'
@@ -47,6 +47,29 @@ describe('React', () => {
4747
})
4848

4949
describe('lifeycle interactions', () => {
50+
it('always uses the latest state', () => {
51+
store = createStore(c => c + 1, -1)
52+
53+
const Comp = () => {
54+
const selector = useCallback(c => c + 1, [])
55+
const value = useSelector(selector)
56+
renderedItems.push(value)
57+
return <div />
58+
}
59+
60+
rtl.render(
61+
<ProviderMock store={store}>
62+
<Comp />
63+
</ProviderMock>
64+
)
65+
66+
expect(renderedItems).toEqual([1])
67+
68+
store.dispatch({ type: '' })
69+
70+
expect(renderedItems).toEqual([1, 2])
71+
})
72+
5073
it('subscribes to the store synchronously', () => {
5174
let rootSubscription
5275

@@ -156,6 +179,39 @@ describe('React', () => {
156179
})
157180
})
158181

182+
it('uses the latest selector', () => {
183+
let selectorId = 0
184+
let forceRender
185+
186+
const Comp = () => {
187+
const [, f] = useReducer(c => c + 1, 0)
188+
forceRender = f
189+
const renderedSelectorId = selectorId++
190+
const value = useSelector(() => renderedSelectorId)
191+
renderedItems.push(value)
192+
return <div />
193+
}
194+
195+
rtl.render(
196+
<ProviderMock store={store}>
197+
<Comp />
198+
</ProviderMock>
199+
)
200+
201+
expect(renderedItems).toEqual([0])
202+
203+
rtl.act(forceRender)
204+
expect(renderedItems).toEqual([0, 1])
205+
206+
rtl.act(() => {
207+
store.dispatch({ type: '' })
208+
})
209+
expect(renderedItems).toEqual([0, 1])
210+
211+
rtl.act(forceRender)
212+
expect(renderedItems).toEqual([0, 1, 2])
213+
})
214+
159215
describe('edge cases', () => {
160216
it('ignores transient errors in selector (e.g. due to stale props)', () => {
161217
const spy = jest.spyOn(console, 'error').mockImplementation(() => {})

0 commit comments

Comments
 (0)