From a5e9a43df643efeca23d0d533a82babedb4da97d Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sat, 3 Jun 2023 00:08:33 +0100 Subject: [PATCH 1/7] add no-op check --- src/components/Context.ts | 5 ++- src/components/Provider.tsx | 11 +++-- src/hooks/useSelector.ts | 83 +++++++++++++++++++++++-------------- 3 files changed, 64 insertions(+), 35 deletions(-) diff --git a/src/components/Context.ts b/src/components/Context.ts index 5c49d5a0e..a0b5cab09 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 { 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..a11eaf55d 100644 --- a/src/components/Provider.tsx +++ b/src/components/Provider.tsx @@ -3,7 +3,7 @@ import { ReactReduxContext, ReactReduxContextValue } from './Context' import { createSubscription } from '../utils/Subscription' import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' import { Action, AnyAction, Store } from 'redux' -import { StabilityCheck } from '../hooks/useSelector' +import { CheckFrequency } from '../hooks/useSelector' export interface ProviderProps { /** @@ -24,7 +24,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 +38,7 @@ function Provider({ children, serverState, stabilityCheck = 'once', + noopCheck = 'once', }: ProviderProps) { const contextValue = useMemo(() => { const subscription = createSubscription(store) @@ -43,8 +47,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/hooks/useSelector.ts b/src/hooks/useSelector.ts index f20ff17fb..0415e908b 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,49 @@ 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 = + // are we safe to use ?? here? + 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 = + // are we safe to use ?? here? + 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 }, From 43c75e257409fd25a9c17fb37a4e880238a5276c Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sat, 3 Jun 2023 17:27:38 +0100 Subject: [PATCH 2/7] add test --- src/hooks/useSelector.ts | 2 - test/hooks/useSelector.spec.tsx | 105 +++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 39 deletions(-) diff --git a/src/hooks/useSelector.ts b/src/hooks/useSelector.ts index 0415e908b..e4c232e7d 100644 --- a/src/hooks/useSelector.ts +++ b/src/hooks/useSelector.ts @@ -90,7 +90,6 @@ export function createSelectorHook(context = ReactReduxContext): UseSelector { const selected = selector(state) if (process.env.NODE_ENV !== 'production') { const finalStabilityCheck = - // are we safe to use ?? here? typeof stabilityCheck === 'undefined' ? globalStabilityCheck : stabilityCheck @@ -114,7 +113,6 @@ export function createSelectorHook(context = ReactReduxContext): UseSelector { } } const finalNoopCheck = - // are we safe to use ?? here? typeof noopCheck === 'undefined' ? globalNoopCheck : noopCheck if ( finalNoopCheck === 'always' || diff --git a/test/hooks/useSelector.spec.tsx b/test/hooks/useSelector.spec.tsx index 20d1e5464..500aded64 100644 --- a/test/hooks/useSelector.spec.tsx +++ b/test/hooks/useSelector.spec.tsx @@ -28,13 +28,19 @@ import type { FunctionComponent, DispatchWithoutAction, ReactNode } from 'react' import type { Store, AnyAction } from 'redux' import { 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.') + ) + }) + }) }) }) From 40b838267ac60198f424d9f4afb6d9b45c4d874b Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sat, 3 Jun 2023 17:55:07 +0100 Subject: [PATCH 3/7] update docs --- docs/api/hooks.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/api/hooks.md b/docs/api/hooks.md index b81341d72..42e448df8 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -311,6 +311,42 @@ There are some differences between the selectors passed to `useSelector()` and a - The selector function does _not_ receive an `ownProps` argument. However, props can be used through closure (see the examples above) or by using a curried selector. - You can use the `equalityFn` option to customize the comparison behavior +#### No-op selector check + +In development, a check is conducted on the result returned by the selector. It warns in 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 select as specifically as possible. + +```ts no-transpile +// this selector returns the entire state, meaning that the component will rerender unnecessarily +const { count, user } = useSelector((state) => state) + +// instead, select only the state you need, calling useSelector as many times as needed +const count = useSelector((state) => state.count) +const user = useSelector((state) => state.user) +``` + +By default, this will only happen when the selector is first called. You can configure the check via context, or per `useSelector` call - either to run the check always, or never. + +```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' }) + // ... +} +``` + +:::info +This check is disabled for production environments. +::: + ## `useDispatch()` ```ts From 94c09f426b967b7c5904a9cb8c75ca0c2c4220ed Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sat, 3 Jun 2023 23:50:31 +0100 Subject: [PATCH 4/7] Apply suggestions from code review Co-authored-by: Matt Sutkowski --- docs/api/hooks.md | 10 +++++----- src/components/Context.ts | 2 +- src/components/Provider.tsx | 2 +- test/hooks/useSelector.spec.tsx | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 42e448df8..9564713d7 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -313,20 +313,20 @@ There are some differences between the selectors passed to `useSelector()` and a #### No-op selector check -In development, a check is conducted on the result returned by the selector. It warns in console if the result is the same as the parameter passed in, i.e. the root state. +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 select as specifically as possible. +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. ```ts no-transpile -// this selector returns the entire state, meaning that the component will rerender unnecessarily +// BAD: this selector returns the entire state, meaning that the component will rerender unnecessarily const { count, user } = useSelector((state) => state) -// instead, select only the state you need, calling useSelector as many times as needed +// GOOD: instead, select only the state you need, calling useSelector as many times as needed const count = useSelector((state) => state.count) const user = useSelector((state) => state.user) ``` -By default, this will only happen when the selector is first called. You can configure the check via context, or per `useSelector` call - either to run the check always, or never. +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" diff --git a/src/components/Context.ts b/src/components/Context.ts index a0b5cab09..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 { CheckFrequency } from '../hooks/useSelector' +import type { CheckFrequency } from '../hooks/useSelector' export interface ReactReduxContextValue< SS = any, diff --git a/src/components/Provider.tsx b/src/components/Provider.tsx index a11eaf55d..4ce5d5b54 100644 --- a/src/components/Provider.tsx +++ b/src/components/Provider.tsx @@ -3,7 +3,7 @@ import { ReactReduxContext, ReactReduxContextValue } from './Context' import { createSubscription } from '../utils/Subscription' import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' import { Action, AnyAction, Store } from 'redux' -import { CheckFrequency } from '../hooks/useSelector' +import type { CheckFrequency } from '../hooks/useSelector' export interface ProviderProps { /** diff --git a/test/hooks/useSelector.spec.tsx b/test/hooks/useSelector.spec.tsx index 500aded64..39d7d5a50 100644 --- a/test/hooks/useSelector.spec.tsx +++ b/test/hooks/useSelector.spec.tsx @@ -26,7 +26,7 @@ import type { } from '../../src/index' import type { FunctionComponent, DispatchWithoutAction, ReactNode } from 'react' import type { Store, AnyAction } from 'redux' -import { UseSelectorOptions } from '../../src/hooks/useSelector' +import type { UseSelectorOptions } from '../../src/hooks/useSelector' // disable checks by default function ProviderMock = AnyAction, S = unknown>({ From b5f7ec9373027b431aca1494aeda6bcd0e84d065 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sat, 3 Jun 2023 23:59:13 +0100 Subject: [PATCH 5/7] add import type rule --- .eslintrc | 32 +++++++++++++++++++++++++------ src/components/Provider.tsx | 8 +++++--- src/components/connect.tsx | 13 ++++++++----- src/connect/wrapMapToProps.ts | 4 ++-- src/hooks/useDispatch.ts | 10 ++++------ src/hooks/useStore.ts | 12 +++++------- src/types.ts | 6 +++--- src/utils/bindActionCreators.ts | 2 +- test/components/Provider.spec.tsx | 3 ++- test/components/connect.spec.tsx | 4 ++-- test/hooks/useSelector.spec.tsx | 8 ++++---- 11 files changed, 62 insertions(+), 40 deletions(-) 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/src/components/Provider.tsx b/src/components/Provider.tsx index 4ce5d5b54..3d8bdf75c 100644 --- a/src/components/Provider.tsx +++ b/src/components/Provider.tsx @@ -1,8 +1,10 @@ -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 type { Action, AnyAction, Store } from 'redux' import type { CheckFrequency } from '../hooks/useSelector' export interface ProviderProps { 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/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 39d7d5a50..ae38a10bb 100644 --- a/test/hooks/useSelector.spec.tsx +++ b/test/hooks/useSelector.spec.tsx @@ -7,25 +7,25 @@ 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 type { Store, AnyAction, Action } from 'redux' import type { UseSelectorOptions } from '../../src/hooks/useSelector' // disable checks by default From f9a0de39989ddd3ff26b431fec9fcbeceab8f5bc Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 12 Jun 2023 22:34:04 -0400 Subject: [PATCH 6/7] Update hooks docs with check details --- docs/api/hooks.md | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 9564713d7..181870a0d 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -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,28 +303,19 @@ function Component() { } ``` -### Comparisons with `connect` - -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 selector normally _should_ return just a single value, and not an object. If you do return an object or an array, be sure to use a memoized selector to avoid unnecessary re-renders. -- The selector function does _not_ receive an `ownProps` argument. However, props can be used through closure (see the examples above) or by using a curried selector. -- You can use the `equalityFn` option to customize the comparison behavior - #### 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. +**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) -const user = useSelector((state) => state.user) +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. @@ -343,9 +335,14 @@ function Component() { } ``` -:::info -This check is disabled for production environments. -::: +### Comparisons with `connect` + +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 selector normally _should_ return just a single value, and not an object. If you do return an object or an array, be sure to use a memoized selector to avoid unnecessary re-renders. +- The selector function does _not_ receive an `ownProps` argument. However, props can be used through closure (see the examples above) or by using a curried selector. +- You can use the `equalityFn` option to customize the comparison behavior ## `useDispatch()` From a18e8a904584050cbe4d52b689d5d5cd308179d5 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 12 Jun 2023 22:59:17 -0400 Subject: [PATCH 7/7] Update React docs links --- docs/api/hooks.md | 4 ++-- docs/introduction/getting-started.md | 2 +- docs/tutorials/quick-start.md | 2 +- docs/tutorials/typescript.md | 2 +- docs/using-react-redux/accessing-store.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 181870a0d..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. @@ -373,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`.