Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce unnecessary calls to useSelector selector #1803

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/hooks/useSelector.js
Expand Up @@ -65,6 +65,11 @@ function useSelectorWithStoreAndSubscription(
function checkForUpdates() {
try {
const newStoreState = store.getState()
// Avoid calling selector multiple times if the store's state has not changed
if (newStoreState === latestStoreState.current) {
return
}

const newSelectedState = latestSelector.current(newStoreState)

if (equalityFn(newSelectedState, latestSelectedState.current)) {
Expand Down
37 changes: 35 additions & 2 deletions test/hooks/useSelector.spec.js
Expand Up @@ -45,14 +45,14 @@ describe('React', () => {
})

expect(result.current).toEqual(0)
expect(selector).toHaveBeenCalledTimes(2)
expect(selector).toHaveBeenCalledTimes(1)

act(() => {
store.dispatch({ type: '' })
})

expect(result.current).toEqual(1)
expect(selector).toHaveBeenCalledTimes(3)
expect(selector).toHaveBeenCalledTimes(2)
})
})

Expand Down Expand Up @@ -246,6 +246,39 @@ describe('React', () => {

expect(renderedItems.length).toBe(1)
})

it('calls selector exactly once on mount and on update', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✋ can you also add a test that does force the selector to run a second time? This should be doable by rendering a child component inside of a parent, and dispatching inside of a useLayoutEffect in the child. They mount in the same pass, and the child's useLayoutEffect should run before the parent's does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, added a test here and in the other PR with a useLayoutEffect that verifies the selector is re-run when state changes during initial mount

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I'm about to head out on holiday and won't be checking back on this until Sep 7th, so don't be alarmed/think this is abandoned if I don't get back to any additional follow-ups. Thank you for all the help!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, thanks!

store = createStore(({ count } = { count: 0 }) => ({
count: count + 1,
}))

let numCalls = 0
const selector = (s) => {
numCalls += 1
return s.count
}
const renderedItems = []

const Comp = () => {
const value = useSelector(selector)
renderedItems.push(value)
return <div />
}

rtl.render(
<ProviderMock store={store}>
<Comp />
</ProviderMock>
)

expect(numCalls).toBe(1)
expect(renderedItems.length).toEqual(1)

store.dispatch({ type: '' })

expect(numCalls).toBe(2)
expect(renderedItems.length).toEqual(2)
})
})

it('uses the latest selector', () => {
Expand Down