Repository: reduxjs/react-redux
Pull request #1251 (2019-04-22 14:18:30)

add `deps` to `useSelector` to allow controlling stability of selector
This PR adds a `deps` parameter to `useSelector` as discussed [here](https://github.com/reduxjs/react-redux/pull/1248#issuecomment-485335594).

It also fixes an issue with staleness of the selector.

netlify[bot]: Deploy preview for *react-redux-docs* ready!

Built with commit 143ca4e03cc5624049374f118655e8a1aa377541

https://deploy-preview-1251--react-redux-docs.netlify.com
Dudeonyx: If no dependency array is passed what happens?
markerikson: @Dudeonyx : the latest passed-in selector should get used, instead of a previous one.
markerikson: LGTM.  I'll try to get alpha.1 published shortly.

Patch:
diff --git a/src/hooks/useSelector.js b/src/hooks/useSelector.js
index 397a93c..70fb709 100644
--- a/src/hooks/useSelector.js
+++ b/src/hooks/useSelector.js
@@ -18,8 +18,14 @@ const useIsomorphicLayoutEffect =
 /**
  * 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.
+ *
+ * This hook takes a dependencies array as an optional second argument,
+ * which when passed ensures referential stability of the selector (this is primarily
+ * useful if you provide a selector that memoizes values).
  * 
  * @param {Function} selector the selector function
+ * @param {any[]} deps (optional) dependencies array to control referential stability
+ * of the selector
  * 
  * @returns {any} the selected state
  *
@@ -35,7 +41,7 @@ export const CounterComponent = () => {
 }
 ```
  */
-export function useSelector(selector) {
+export function useSelector(selector, deps) {
   invariant(selector, `You must pass a selector to useSelectors`)
 
   const { store, subscription: contextSub } = useReduxContext()
@@ -46,13 +52,15 @@ export function useSelector(selector) {
     contextSub
   ])
 
+  const memoizedSelector = useMemo(() => selector, deps)
+
   const latestSubscriptionCallbackError = useRef()
-  const latestSelector = useRef(selector)
+  const latestSelector = useRef(memoizedSelector)
 
   let selectedState = undefined
 
   try {
-    selectedState = latestSelector.current(store.getState())
+    selectedState = memoizedSelector(store.getState())
   } catch (err) {
     let errorMessage = `An error occured while selecting the store state: ${
       err.message
@@ -70,7 +78,7 @@ export function useSelector(selector) {
   const latestSelectedState = useRef(selectedState)
 
   useIsomorphicLayoutEffect(() => {
-    latestSelector.current = selector
+    latestSelector.current = memoizedSelector
     latestSelectedState.current = selectedState
     latestSubscriptionCallbackError.current = undefined
   })


Test patch:
diff --git a/test/hooks/useSelector.spec.js b/test/hooks/useSelector.spec.js
index a2fa272..411829c 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, { useReducer } from 'react'
 import { createStore } from 'redux'
 import * as rtl from 'react-testing-library'
 import { Provider as ProviderMock, useSelector } from '../../src/index.js'
@@ -165,15 +165,40 @@ describe('React', () => {
 
           expect(renderedItems.length).toBe(1)
         })
+
+        it('re-uses the selector if deps do not change', () => {
+          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 <div />
+          }
+
+          rtl.render(
+            <ProviderMock store={store}>
+              <Comp />
+            </ProviderMock>
+          )
+
+          rtl.act(forceRender)
+
+          // this line verifies the susbcription callback uses the same memoized selector and therefore
+          // does not cause a re-render
+          store.dispatch({ type: '' })
+
+          expect(renderedItems).toEqual([0, 0])
+        })
       })
 
       describe('edge cases', () => {
         it('ignores transient errors in selector (e.g. due to stale props)', () => {
-          // TODO Not sure this test is really testing what we want.
-          // TODO The parent re-renders, which causes the child to re-run the selector anyway and throw the error.
-          // TODO Had to flip the assertion for now. Probably needs to be rethought.
-
           const spy = jest.spyOn(console, 'error').mockImplementation(() => {})
+
           const Parent = () => {
             const count = useSelector(s => s.count)
             return <Child parentCount={count} />
@@ -197,9 +222,7 @@ describe('React', () => {
             </ProviderMock>
           )
 
-          expect(() => store.dispatch({ type: '' })).toThrowError(
-            /while selecting the store state/
-          )
+          expect(() => store.dispatch({ type: '' })).not.toThrowError()
 
           spy.mockRestore()
         })