diff --git a/.eslintrc b/.eslintrc index afacdf656..05bd4959c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -11,10 +11,16 @@ "react": { "version": "detect" }, - "import/ignore": ["react-native"], + "import/ignore": [ + "react-native" + ], "import/resolver": { "node": { - "extensions": [".js", ".ts", ".tsx"] + "extensions": [ + ".js", + ".ts", + ".tsx" + ] } } }, @@ -38,12 +44,26 @@ "react/jsx-wrap-multilines": 2, "react/no-string-refs": 0, "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error"], + "@typescript-eslint/no-unused-vars": [ + "error" + ], "no-redeclare": "off", - "@typescript-eslint/no-redeclare": ["error"] + "@typescript-eslint/no-redeclare": [ + "error" + ], + "@typescript-eslint/consistent-type-imports": [ + "error", + { + "prefer": "type-imports" + } + ] }, - "plugins": ["@typescript-eslint", "import", "react"], + "plugins": [ + "@typescript-eslint", + "import", + "react" + ], "globals": { "JSX": true } -} +} \ No newline at end of file diff --git a/docs/api/hooks.md b/docs/api/hooks.md index b81341d72..90ba18fdd 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -10,7 +10,7 @@ description: 'API > Hooks: the `useSelector` and `useDispatch` hooks`' # Hooks -React's new ["hooks" APIs](https://reactjs.org/docs/hooks-intro.html) give function components the ability to use local component state, execute side effects, and more. React also lets us write [custom hooks](https://reactjs.org/docs/hooks-custom.html), which let us extract reusable hooks to add our own behavior on top of React's built-in hooks. +React's ["hooks" APIs](https://react.dev/reference/react#) give function components the ability to use local component state, execute side effects, and more. React also lets us write [custom hooks](https://react.dev/learn/reusing-logic-with-custom-hooks#extracting-your-own-custom-hook-from-a-component), which let us extract reusable hooks to add our own behavior on top of React's built-in hooks. React Redux includes its own custom hook APIs, which allow your React components to subscribe to the Redux store and dispatch actions. @@ -48,11 +48,12 @@ From there, you may import any of the listed React Redux hooks APIs and use them type RootState = ReturnType type SelectorFn = (state: RootState) => Selected type EqualityFn = (a: any, b: any) => boolean -export type StabilityCheck = 'never' | 'once' | 'always' +export type CheckFrequency = 'never' | 'once' | 'always' interface UseSelectorOptions { equalityFn?: EqualityFn - stabilityCheck?: StabilityCheck + stabilityCheck?: CheckFrequency + noopCheck?: CheckFrequency } const result: Selected = useSelector( @@ -272,7 +273,7 @@ These checks were first added in v8.1.0 In development, the provided selector function is run an extra time with the same parameter during the first call to `useSelector`, and warns in the console if the selector returns a different result (based on the `equalityFn` provided). -This is important, as a selector returning that returns a different result reference with the same parameter will cause unnecessary rerenders. +This is important, as **a selector that returns a different result reference when called again with the same inputs will cause unnecessary rerenders**. ```ts // this selector will return a new object reference whenever called, @@ -302,6 +303,38 @@ function Component() { } ``` +#### No-op selector check + +In development, a check is conducted on the result returned by the selector. It warns in the console if the result is the same as the parameter passed in, i.e. the root state. + +**A `useSelector` call returning the entire root state is almost always a mistake**, as it means the component will rerender whenever _anything_ in state changes. Selectors should be as granular as possible, like `state => state.some.nested.field`. + +```ts no-transpile +// BAD: this selector returns the entire state, meaning that the component will rerender unnecessarily +const { count, user } = useSelector((state) => state) + +// GOOD: instead, select only the state you need, calling useSelector as many times as needed +const count = useSelector((state) => state.count.value) +const user = useSelector((state) => state.auth.currentUser) +``` + +By default, this will only happen when the selector is first called. You can configure the check in the Provider or at each `useSelector` call. + +```tsx title="Global setting via context" + + {children} + +``` + +```tsx title="Individual hook setting" +function Component() { + const count = useSelector(selectCount, { noopCheck: 'never' }) + // run once (default) + const user = useSelector(selectUser, { noopCheck: 'once' }) + // ... +} +``` + ### Comparisons with `connect` There are some differences between the selectors passed to `useSelector()` and a `mapState` function: @@ -340,7 +373,7 @@ export const CounterComponent = ({ value }) => { } ``` -When passing a callback using `dispatch` to a child component, you may sometimes want to memoize it with [`useCallback`](https://reactjs.org/docs/hooks-reference.html#usecallback). _If_ the child component is trying to optimize render behavior using `React.memo()` or similar, this avoids unnecessary rendering of child components due to the changed callback reference. +When passing a callback using `dispatch` to a child component, you may sometimes want to memoize it with [`useCallback`](https://react.dev/reference/react/useCallback). _If_ the child component is trying to optimize render behavior using `React.memo()` or similar, this avoids unnecessary rendering of child components due to the changed callback reference. ```jsx import React, { useCallback } from 'react' diff --git a/docs/introduction/getting-started.md b/docs/introduction/getting-started.md index 83b420337..11da5346a 100644 --- a/docs/introduction/getting-started.md +++ b/docs/introduction/getting-started.md @@ -13,7 +13,7 @@ import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css' # Getting Started with React Redux -[React Redux](https://github.com/reduxjs/react-redux) is the official [React](https://reactjs.org/) UI bindings layer for [Redux](https://redux.js.org/). It lets your React components read data from a Redux store, and dispatch actions to the store to update state. +[React Redux](https://github.com/reduxjs/react-redux) is the official [React](https://react.dev/) UI bindings layer for [Redux](https://redux.js.org/). It lets your React components read data from a Redux store, and dispatch actions to the store to update state. ## Installation diff --git a/docs/tutorials/quick-start.md b/docs/tutorials/quick-start.md index 4e419f170..e0802e135 100644 --- a/docs/tutorials/quick-start.md +++ b/docs/tutorials/quick-start.md @@ -18,7 +18,7 @@ hide_title: true :::info Prerequisites - Familiarity with [ES6 syntax and features](https://www.taniarascia.com/es6-syntax-and-feature-overview/) -- Knowledge of React terminology: [JSX](https://reactjs.org/docs/introducing-jsx.html), [State](https://reactjs.org/docs/state-and-lifecycle.html), [Function Components, Props](https://reactjs.org/docs/components-and-props.html), and [Hooks](https://reactjs.org/docs/hooks-intro.html) +- Knowledge of React terminology: [JSX](https://react.dev/learn/writing-markup-with-jsx), [State](https://react.dev/learn/state-a-components-memory), [Function Components, Props](https://react.dev/learn/passing-props-to-a-component), and [Hooks](https://react.dev/reference/react#) - Understanding of [Redux terms and concepts](https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow) ::: diff --git a/docs/tutorials/typescript.md b/docs/tutorials/typescript.md index 600b64281..9cf79a26b 100644 --- a/docs/tutorials/typescript.md +++ b/docs/tutorials/typescript.md @@ -17,7 +17,7 @@ hide_title: true :::info Prerequisites -- Knowledge of React [Hooks](https://reactjs.org/docs/hooks-intro.html) +- Knowledge of React [Hooks](https://react.dev/reference/react#) - Understanding of [Redux terms and concepts](https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow) - Understanding of TypeScript syntax and concepts diff --git a/docs/using-react-redux/accessing-store.md b/docs/using-react-redux/accessing-store.md index 970819036..59075610f 100644 --- a/docs/using-react-redux/accessing-store.md +++ b/docs/using-react-redux/accessing-store.md @@ -22,7 +22,7 @@ connected components, or access the store directly. Here are some examples of ho ## Understanding Context Usage -Internally, React Redux uses [React's "context" feature](https://reactjs.org/docs/context.html) to make the +Internally, React Redux uses [React's "context" feature](https://react.dev/learn/passing-data-deeply-with-context) to make the Redux store accessible to deeply nested connected components. As of React Redux version 6, this is normally handled by a single default context object instance generated by `React.createContext()`, called `ReactReduxContext`. diff --git a/src/components/Context.ts b/src/components/Context.ts index 5c49d5a0e..8454e09f1 100644 --- a/src/components/Context.ts +++ b/src/components/Context.ts @@ -2,7 +2,7 @@ import { createContext } from 'react' import type { Context } from 'react' import type { Action, AnyAction, Store } from 'redux' import type { Subscription } from '../utils/Subscription' -import { StabilityCheck } from '../hooks/useSelector' +import type { CheckFrequency } from '../hooks/useSelector' export interface ReactReduxContextValue< SS = any, @@ -11,7 +11,8 @@ export interface ReactReduxContextValue< store: Store subscription: Subscription getServerState?: () => SS - stabilityCheck: StabilityCheck + stabilityCheck: CheckFrequency + noopCheck: CheckFrequency } let realContext: Context | null = null diff --git a/src/components/Provider.tsx b/src/components/Provider.tsx index 55cb71b80..3d8bdf75c 100644 --- a/src/components/Provider.tsx +++ b/src/components/Provider.tsx @@ -1,9 +1,11 @@ -import React, { Context, ReactNode, useMemo } from 'react' -import { ReactReduxContext, ReactReduxContextValue } from './Context' +import type { Context, ReactNode } from 'react' +import React, { useMemo } from 'react' +import type { ReactReduxContextValue } from './Context' +import { ReactReduxContext } from './Context' import { createSubscription } from '../utils/Subscription' import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' -import { Action, AnyAction, Store } from 'redux' -import { StabilityCheck } from '../hooks/useSelector' +import type { Action, AnyAction, Store } from 'redux' +import type { CheckFrequency } from '../hooks/useSelector' export interface ProviderProps { /** @@ -24,7 +26,10 @@ export interface ProviderProps { context?: Context> /** Global configuration for the `useSelector` stability check */ - stabilityCheck?: StabilityCheck + stabilityCheck?: CheckFrequency + + /** Global configuration for the `useSelector` no-op check */ + noopCheck?: CheckFrequency children: ReactNode } @@ -35,6 +40,7 @@ function Provider({ children, serverState, stabilityCheck = 'once', + noopCheck = 'once', }: ProviderProps) { const contextValue = useMemo(() => { const subscription = createSubscription(store) @@ -43,8 +49,9 @@ function Provider({ subscription, getServerState: serverState ? () => serverState : undefined, stabilityCheck, + noopCheck, } - }, [store, serverState, stabilityCheck]) + }, [store, serverState, stabilityCheck, noopCheck]) const previousState = useMemo(() => store.getState(), [store]) diff --git a/src/components/connect.tsx b/src/components/connect.tsx index 5e318a5c4..77fb273d2 100644 --- a/src/components/connect.tsx +++ b/src/components/connect.tsx @@ -1,6 +1,7 @@ /* eslint-disable valid-jsdoc, @typescript-eslint/no-unused-vars */ import hoistStatics from 'hoist-non-react-statics' -import React, { ComponentType, useContext, useMemo, useRef } from 'react' +import type { ComponentType } from 'react' +import React, { useContext, useMemo, useRef } from 'react' import { isValidElementType, isContextConsumer } from 'react-is' import type { Store } from 'redux' @@ -14,27 +15,29 @@ import type { ConnectPropsMaybeWithoutContext, } from '../types' -import defaultSelectorFactory, { +import type { MapStateToPropsParam, MapDispatchToPropsParam, MergeProps, MapDispatchToPropsNonObject, SelectorFactoryOptions, } from '../connect/selectorFactory' +import defaultSelectorFactory from '../connect/selectorFactory' import { mapDispatchToPropsFactory } from '../connect/mapDispatchToProps' import { mapStateToPropsFactory } from '../connect/mapStateToProps' import { mergePropsFactory } from '../connect/mergeProps' -import { createSubscription, Subscription } from '../utils/Subscription' +import type { Subscription } from '../utils/Subscription' +import { createSubscription } from '../utils/Subscription' import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' import shallowEqual from '../utils/shallowEqual' import warning from '../utils/warning' -import { - ReactReduxContext, +import type { ReactReduxContextValue, ReactReduxContextInstance, } from './Context' +import { ReactReduxContext } from './Context' import type { uSES } from '../utils/useSyncExternalStore' import { notInitialized } from '../utils/useSyncExternalStore' diff --git a/src/connect/wrapMapToProps.ts b/src/connect/wrapMapToProps.ts index 5b8387368..9dad1e74a 100644 --- a/src/connect/wrapMapToProps.ts +++ b/src/connect/wrapMapToProps.ts @@ -1,6 +1,6 @@ -import { ActionCreatorsMapObject, Dispatch, ActionCreator } from 'redux' +import type { ActionCreatorsMapObject, Dispatch, ActionCreator } from 'redux' -import { FixTypeLater } from '../types' +import type { FixTypeLater } from '../types' import verifyPlainObject from '../utils/verifyPlainObject' type AnyState = { [key: string]: any } diff --git a/src/hooks/useDispatch.ts b/src/hooks/useDispatch.ts index ef22881bb..f747e1baa 100644 --- a/src/hooks/useDispatch.ts +++ b/src/hooks/useDispatch.ts @@ -1,10 +1,8 @@ -import { Action, AnyAction, Dispatch } from 'redux' -import { Context } from 'react' +import type { Action, AnyAction, Dispatch } from 'redux' +import type { Context } from 'react' -import { - ReactReduxContext, - ReactReduxContextValue, -} from '../components/Context' +import type { ReactReduxContextValue } from '../components/Context' +import { ReactReduxContext } from '../components/Context' import { useStore as useDefaultStore, createStoreHook } from './useStore' /** diff --git a/src/hooks/useSelector.ts b/src/hooks/useSelector.ts index f20ff17fb..e4c232e7d 100644 --- a/src/hooks/useSelector.ts +++ b/src/hooks/useSelector.ts @@ -9,11 +9,12 @@ import type { EqualityFn, NoInfer } from '../types' import type { uSESWS } from '../utils/useSyncExternalStore' import { notInitialized } from '../utils/useSyncExternalStore' -export type StabilityCheck = 'never' | 'once' | 'always' +export type CheckFrequency = 'never' | 'once' | 'always' export interface UseSelectorOptions { equalityFn?: EqualityFn - stabilityCheck?: StabilityCheck + stabilityCheck?: CheckFrequency + noopCheck?: CheckFrequency } interface UseSelector { @@ -52,10 +53,13 @@ export function createSelectorHook(context = ReactReduxContext): UseSelector { | EqualityFn> | UseSelectorOptions> = {} ): Selected { - const { equalityFn = refEquality, stabilityCheck = undefined } = - typeof equalityFnOrOptions === 'function' - ? { equalityFn: equalityFnOrOptions } - : equalityFnOrOptions + const { + equalityFn = refEquality, + stabilityCheck = undefined, + noopCheck = undefined, + } = typeof equalityFnOrOptions === 'function' + ? { equalityFn: equalityFnOrOptions } + : equalityFnOrOptions if (process.env.NODE_ENV !== 'production') { if (!selector) { throw new Error(`You must pass a selector to useSelector`) @@ -75,6 +79,7 @@ export function createSelectorHook(context = ReactReduxContext): UseSelector { subscription, getServerState, stabilityCheck: globalStabilityCheck, + noopCheck: globalNoopCheck, } = useReduxContext()! const firstRun = useRef(true) @@ -83,31 +88,47 @@ export function createSelectorHook(context = ReactReduxContext): UseSelector { { [selector.name](state: TState) { const selected = selector(state) - const finalStabilityCheck = - // are we safe to use ?? here? - typeof stabilityCheck === 'undefined' - ? globalStabilityCheck - : stabilityCheck - if ( - process.env.NODE_ENV !== 'production' && - (finalStabilityCheck === 'always' || - (finalStabilityCheck === 'once' && firstRun.current)) - ) { - const toCompare = selector(state) - if (!equalityFn(selected, toCompare)) { - console.warn( - 'Selector ' + - (selector.name || 'unknown') + - ' returned a different result when called with the same parameters. This can lead to unnecessary rerenders.' + - '\nSelectors that return a new reference (such as an object or an array) should be memoized: https://redux.js.org/usage/deriving-data-selectors#optimizing-selectors-with-memoization', - { - state, - selected, - selected2: toCompare, - } - ) + if (process.env.NODE_ENV !== 'production') { + const finalStabilityCheck = + typeof stabilityCheck === 'undefined' + ? globalStabilityCheck + : stabilityCheck + if ( + finalStabilityCheck === 'always' || + (finalStabilityCheck === 'once' && firstRun.current) + ) { + const toCompare = selector(state) + if (!equalityFn(selected, toCompare)) { + console.warn( + 'Selector ' + + (selector.name || 'unknown') + + ' returned a different result when called with the same parameters. This can lead to unnecessary rerenders.' + + '\nSelectors that return a new reference (such as an object or an array) should be memoized: https://redux.js.org/usage/deriving-data-selectors#optimizing-selectors-with-memoization', + { + state, + selected, + selected2: toCompare, + } + ) + } + } + const finalNoopCheck = + typeof noopCheck === 'undefined' ? globalNoopCheck : noopCheck + if ( + finalNoopCheck === 'always' || + (finalNoopCheck === 'once' && firstRun.current) + ) { + // @ts-ignore + if (selected === state) { + console.warn( + 'Selector ' + + (selector.name || 'unknown') + + ' returned the root state when called. This can lead to unnecessary rerenders.' + + '\nSelectors that return the entire state are almost certainly a mistake, as they will cause a rerender whenever *anything* in state changes.' + ) + } } - firstRun.current = false + if (firstRun.current) firstRun.current = false } return selected }, diff --git a/src/hooks/useStore.ts b/src/hooks/useStore.ts index cf5d8868b..1d92e397e 100644 --- a/src/hooks/useStore.ts +++ b/src/hooks/useStore.ts @@ -1,12 +1,10 @@ -import { Context } from 'react' -import { Action as BasicAction, AnyAction, Store } from 'redux' +import type { Context } from 'react' +import type { Action as BasicAction, AnyAction, Store } from 'redux' +import type { ReactReduxContextValue } from '../components/Context' +import { ReactReduxContext } from '../components/Context' import { - ReactReduxContext, - ReactReduxContextValue, -} from '../components/Context' -import { - createReduxContextHook, useReduxContext as useDefaultReduxContext, + createReduxContextHook, } from './useReduxContext' /** diff --git a/src/types.ts b/src/types.ts index 90c24b951..5a1f0c4c6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,17 +1,17 @@ -import { +import type { ClassAttributes, ComponentClass, ComponentType, FunctionComponent, } from 'react' -import { Action, AnyAction, Dispatch } from 'redux' +import type { Action, AnyAction, Dispatch } from 'redux' import type { NonReactStatics } from 'hoist-non-react-statics' import type { ConnectProps } from './components/connect' -import { UseSelectorOptions } from './hooks/useSelector' +import type { UseSelectorOptions } from './hooks/useSelector' export type FixTypeLater = any diff --git a/src/utils/bindActionCreators.ts b/src/utils/bindActionCreators.ts index fa50581bc..0d5f3d8a7 100644 --- a/src/utils/bindActionCreators.ts +++ b/src/utils/bindActionCreators.ts @@ -1,4 +1,4 @@ -import { ActionCreatorsMapObject, Dispatch } from 'redux' +import type { ActionCreatorsMapObject, Dispatch } from 'redux' export default function bindActionCreators( actionCreators: ActionCreatorsMapObject, diff --git a/test/components/Provider.spec.tsx b/test/components/Provider.spec.tsx index dbb4aa95e..5bfddec3d 100644 --- a/test/components/Provider.spec.tsx +++ b/test/components/Provider.spec.tsx @@ -1,6 +1,7 @@ /*eslint-disable react/prop-types*/ -import React, { Component, Dispatch } from 'react' +import type { Dispatch } from 'react' +import React, { Component } from 'react' import ReactDOM from 'react-dom' import { createStore } from 'redux' import { Provider, connect, ReactReduxContext } from '../../src/index' diff --git a/test/components/connect.spec.tsx b/test/components/connect.spec.tsx index 310895509..ad755768e 100644 --- a/test/components/connect.spec.tsx +++ b/test/components/connect.spec.tsx @@ -1,11 +1,11 @@ /*eslint-disable react/prop-types*/ -import React, { Component, MouseEvent } from 'react' +import React, { Component } from 'react' import { createStore, applyMiddleware } from 'redux' import { Provider as ProviderMock, connect } from '../../src/index' import * as rtl from '@testing-library/react' import '@testing-library/jest-dom/extend-expect' -import type { ReactNode, Dispatch, ElementType } from 'react' +import type { ReactNode, Dispatch, ElementType, MouseEvent } from 'react' import type { Store, Dispatch as ReduxDispatch, diff --git a/test/hooks/useSelector.spec.tsx b/test/hooks/useSelector.spec.tsx index 20d1e5464..ae38a10bb 100644 --- a/test/hooks/useSelector.spec.tsx +++ b/test/hooks/useSelector.spec.tsx @@ -7,34 +7,40 @@ import React, { useState, useContext, } from 'react' -import { Action, createStore } from 'redux' +import { createStore } from 'redux' import * as rtl from '@testing-library/react' import { Provider, - ProviderProps, useSelector, useDispatch, shallowEqual, connect, createSelectorHook, ReactReduxContext, - Subscription, } from '../../src/index' import type { TypedUseSelectorHook, ReactReduxContextValue, + ProviderProps, + Subscription, } from '../../src/index' import type { FunctionComponent, DispatchWithoutAction, ReactNode } from 'react' -import type { Store, AnyAction } from 'redux' -import { UseSelectorOptions } from '../../src/hooks/useSelector' +import type { Store, AnyAction, Action } from 'redux' +import type { UseSelectorOptions } from '../../src/hooks/useSelector' -// most of these tests depend on selectors being run once, which stabilityCheck doesn't do -// rather than specify it every time, let's make a new "default" here +// disable checks by default function ProviderMock = AnyAction, S = unknown>({ stabilityCheck = 'never', + noopCheck = 'never', ...props }: ProviderProps) { - return + return ( + + ) } const IS_REACT_18 = React.version.startsWith('18') @@ -739,36 +745,38 @@ describe('React', () => { }) describe('Development mode checks', () => { + const consoleSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}) + afterEach(() => { + consoleSpy.mockClear() + }) + afterAll(() => { + consoleSpy.mockRestore() + }) + + const RenderSelector = ({ + selector, + options, + }: { + selector: (state: NormalStateType) => unknown + options?: UseSelectorOptions + }) => { + useSelector(selector, options) + return null + } describe('selector result stability check', () => { const selector = jest.fn((state: NormalStateType) => state.count) - const consoleSpy = jest - .spyOn(console, 'warn') - .mockImplementation(() => {}) afterEach(() => { - consoleSpy.mockClear() selector.mockClear() }) - afterAll(() => { - consoleSpy.mockRestore() - }) - - const RenderSelector = ({ - selector, - options, - }: { - selector: (state: NormalStateType) => unknown - options?: UseSelectorOptions - }) => { - useSelector(selector, options) - return null - } it('calls a selector twice, and warns in console if it returns a different result', () => { rtl.render( - + - + ) expect(selector).toHaveBeenCalledTimes(2) @@ -780,9 +788,9 @@ describe('React', () => { const unstableSelector = jest.fn(() => Math.random()) rtl.render( - + - + ) expect(selector).toHaveBeenCalledTimes(2) @@ -806,12 +814,12 @@ describe('React', () => { })) rtl.render( - + - + ) expect(unstableSelector).toHaveBeenCalledTimes(2) @@ -819,9 +827,9 @@ describe('React', () => { }) it('by default will only check on first selector call', () => { rtl.render( - + - + ) expect(selector).toHaveBeenCalledTimes(2) @@ -834,9 +842,9 @@ describe('React', () => { }) it('disables check if context or hook specifies', () => { rtl.render( - + - + ) expect(selector).toHaveBeenCalledTimes(1) @@ -846,21 +854,21 @@ describe('React', () => { selector.mockClear() rtl.render( - + - + ) expect(selector).toHaveBeenCalledTimes(1) }) it('always runs check if context or hook specifies', () => { rtl.render( - + - + ) expect(selector).toHaveBeenCalledTimes(2) @@ -876,12 +884,12 @@ describe('React', () => { selector.mockClear() rtl.render( - + - + ) expect(selector).toHaveBeenCalledTimes(2) @@ -893,6 +901,29 @@ describe('React', () => { expect(selector).toHaveBeenCalledTimes(4) }) }) + describe('no-op selector check', () => { + it('warns for selectors that return the entire root state', () => { + rtl.render( + + state.count} /> + + ) + + expect(consoleSpy).not.toHaveBeenCalled() + + rtl.cleanup() + + rtl.render( + + state} /> + + ) + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('returned the root state when called.') + ) + }) + }) }) })