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

Add warning for selectors that return the entire state #2022

Merged
merged 7 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
}
},
Expand All @@ -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
}
}
}
43 changes: 38 additions & 5 deletions docs/api/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -48,11 +48,12 @@ From there, you may import any of the listed React Redux hooks APIs and use them
type RootState = ReturnType<typeof store.getState>
type SelectorFn = <Selected>(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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"
<Provider store={store} noopCheck="always">
{children}
</Provider>
```

```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:
Expand Down Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion docs/introduction/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

:::
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/using-react-redux/accessing-store.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
5 changes: 3 additions & 2 deletions src/components/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -11,7 +11,8 @@ export interface ReactReduxContextValue<
store: Store<SS, A>
subscription: Subscription
getServerState?: () => SS
stabilityCheck: StabilityCheck
stabilityCheck: CheckFrequency
noopCheck: CheckFrequency
}

let realContext: Context<ReactReduxContextValue> | null = null
Expand Down
19 changes: 13 additions & 6 deletions src/components/Provider.tsx
Original file line number Diff line number Diff line change
@@ -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<A extends Action = AnyAction, S = unknown> {
/**
Expand All @@ -24,7 +26,10 @@ export interface ProviderProps<A extends Action = AnyAction, S = unknown> {
context?: Context<ReactReduxContextValue<S, A>>

/** Global configuration for the `useSelector` stability check */
stabilityCheck?: StabilityCheck
stabilityCheck?: CheckFrequency

/** Global configuration for the `useSelector` no-op check */
noopCheck?: CheckFrequency

children: ReactNode
}
Expand All @@ -35,6 +40,7 @@ function Provider<A extends Action = AnyAction, S = unknown>({
children,
serverState,
stabilityCheck = 'once',
noopCheck = 'once',
}: ProviderProps<A, S>) {
const contextValue = useMemo(() => {
const subscription = createSubscription(store)
Expand All @@ -43,8 +49,9 @@ function Provider<A extends Action = AnyAction, S = unknown>({
subscription,
getServerState: serverState ? () => serverState : undefined,
stabilityCheck,
noopCheck,
}
}, [store, serverState, stabilityCheck])
}, [store, serverState, stabilityCheck, noopCheck])

const previousState = useMemo(() => store.getState(), [store])

Expand Down
13 changes: 8 additions & 5 deletions src/components/connect.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand Down
4 changes: 2 additions & 2 deletions src/connect/wrapMapToProps.ts
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down
10 changes: 4 additions & 6 deletions src/hooks/useDispatch.ts
Original file line number Diff line number Diff line change
@@ -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'

/**
Expand Down
Loading