From 6304f0129c46c25b410287d6595c521dfbfeb7ee Mon Sep 17 00:00:00 2001 From: Josep M Sobrepere Date: Thu, 2 May 2019 16:25:31 +0200 Subject: [PATCH] Remove deps of useSelector --- docs/api/hooks.md | 64 +++++++++++++++++++++++++++++++--- src/hooks/useSelector.js | 14 +++----- test/hooks/useSelector.spec.js | 7 ++-- 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 4485cad09..e249e3915 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -33,7 +33,7 @@ From there, you may import any of the listed React Redux hooks APIs and use them ## `useSelector()` ```js -const result : any = useSelector(selector : Function, deps : any[]) +const result : any = useSelector(selector : Function) ``` Allows you to extract data from the Redux store state, using a selector function. @@ -43,7 +43,6 @@ The selector is approximately equivalent to the [`mapStateToProps` argument to ` However, there are some differences between the selectors passed to `useSelector()` and a `mapState` function: - The selector may return any value as a result, not just an object. The return value of the selector will be used as the return value of the `useSelector()` hook. -- The selector function used will be based on the `deps` array. If no deps array is provided, the latest passed-in selector function will be used when the component renders, and also when any actions are dispatched before the next render. If a deps array is provided, the last saved selector will be used, and that selector will be overwritten whenever the deps array contents have changed. - When an action is dispatched, `useSelector()` will do a shallow comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, they component will not re-render. - The selector function does _not_ receive an `ownProps` argument. If you wish to use props within the selector function to determine what values to extract, you should call the React [`useMemo()`](https://reactjs.org/docs/hooks-reference.html#usememo) or [`useCallback()`](https://reactjs.org/docs/hooks-reference.html#usecallback) hooks yourself to create a version of the selector that will be re-created whenever the props it depends on change. @@ -68,16 +67,71 @@ export const CounterComponent = () => { Using props to determine what to extract: ```jsx -import React from 'react' +import React, {useCallback} from 'react' import { useSelector } from 'react-redux' -export const TodoListItem = props => ( - const todo = useSelector(state => state.todos[props.id], [props.id]) +export const TodoListItem = ({id}) => ( + const selector = useCallback(state => state.todos[id], [id]) + const todo = useSelector(selector) return
{todo.text}
} ``` +With Reselect: + +```jsx +// ducks/todos.js +import { createSelector } from 'reselect' + +export const selectPendingTodos = createSelector( + (state) => state.todos, + (todos) => todos.filter(todo => todo.completed !== true) +) + +export const selectNPendingTodos = createSelector( + selectPendingTodos, + pendingTodos => pendingTodos.length +) + +// component.js + +import React from 'react' +import { useSelector } from 'react-redux' +import { selectNPendingTodos } from 'ducks/todos' + +export constPendingTodos = () => { + const nPendingTodos = useSelector(selectNPendingTodos) + return
{nPendingTodos}
+} +``` + +With Reselect and factory-selectors: + +```jsx +// ducks/todos.js +import { createSelector } from 'reselect' + +export const makeSelectTodoById = (id) => createSelector( + (state) => state.todos, + (todos) => todos.filter(todo => todo.id === id) +) + + +// component.js + +import React from 'react' +import { useSelector } from 'react-redux' +import { makeSelectTodoById } from 'ducks/todos' + +export const Todo = ({id}) => { + const todoSelector = useMemo(() => makeSelectTodoById(id), [id]) + const todo = useSelector(todoSelector) + + return
{todo.name}
+} +``` + ## `useActions()` ```js diff --git a/src/hooks/useSelector.js b/src/hooks/useSelector.js index 83e1666f9..8e40cfa10 100644 --- a/src/hooks/useSelector.js +++ b/src/hooks/useSelector.js @@ -24,8 +24,6 @@ const useIsomorphicLayoutEffect = * 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 * @@ -36,11 +34,11 @@ const useIsomorphicLayoutEffect = * import { RootState } from './store' * * export const CounterComponent = () => { - * const counter = useSelector(state => state.counter, []) + * const counter = useSelector(state => state.counter) * return
{counter}
* } */ -export function useSelector(selector, deps) { +export function useSelector(selector) { invariant(selector, `You must pass a selector to useSelectors`) const { store, subscription: contextSub } = useReduxContext() @@ -51,15 +49,13 @@ export function useSelector(selector, deps) { contextSub ]) - const memoizedSelector = useMemo(() => selector, deps) - const latestSubscriptionCallbackError = useRef() - const latestSelector = useRef(memoizedSelector) + const latestSelector = useRef(selector) let selectedState = undefined try { - selectedState = memoizedSelector(store.getState()) + selectedState = selector(store.getState()) } catch (err) { let errorMessage = `An error occured while selecting the store state: ${ err.message @@ -77,7 +73,7 @@ export function useSelector(selector, deps) { const latestSelectedState = useRef(selectedState) useIsomorphicLayoutEffect(() => { - latestSelector.current = memoizedSelector + latestSelector.current = selector latestSelectedState.current = selectedState latestSubscriptionCallbackError.current = undefined }) diff --git a/test/hooks/useSelector.spec.js b/test/hooks/useSelector.spec.js index e0fb95c8b..74043f1c1 100644 --- a/test/hooks/useSelector.spec.js +++ b/test/hooks/useSelector.spec.js @@ -1,6 +1,6 @@ /*eslint-disable react/prop-types*/ -import React, { useReducer } from 'react' +import React, { useReducer, useCallback } from 'react' import { createStore } from 'redux' import { renderHook, act } from 'react-hooks-testing-library' import * as rtl from 'react-testing-library' @@ -155,7 +155,7 @@ describe('React', () => { expect(renderedItems.length).toBe(1) }) - it('re-uses the selector if deps do not change', () => { + it('re-uses the selector if it does not change', () => { let selectorId = 0 let forceRender @@ -163,7 +163,8 @@ describe('React', () => { const [, f] = useReducer(c => c + 1, 0) forceRender = f const renderedSelectorId = selectorId++ - const value = useSelector(() => renderedSelectorId, []) + const selector = useCallback(() => renderedSelectorId, []) + const value = useSelector(selector) renderedItems.push(value) return
}